Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
C
common
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
npm
common
Commits
876ca5fc
Commit
876ca5fc
authored
Jan 24, 2019
by
Vladislav Lagunov
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Исправления в импортах
parent
444c887c
Show whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
10 additions
and
655 deletions
+10
-655
decoder/index.ts
+1
-1
fields/AutoComplete/state-machine.ts
+1
-1
fields/SelectField.tsx
+3
-3
jsonapi/resources.ts
+2
-2
utils/jsonapi/decoders.ts
+1
-271
utils/jsonapi/index.ts
+1
-5
utils/jsonapi/resources.ts
+1
-372
No files found.
decoder/index.ts
View file @
876ca5fc
...
...
@@ -403,8 +403,8 @@ export function optional(key: string|string[], dec: Decoder<any>, def: any): Dec
/**
* Создание декодера перечислением всех допустимых значений
*/
export
function
variants
<
A
extends
Expr
[]
>
(...
array
:
A
):
Variants
<
A
[
number
]
>
;
export
function
variants
<
A
extends
Expr
[]
>
(
array
:
A
):
Variants
<
A
[
number
]
>
;
export
function
variants
<
A
extends
Expr
[]
>
(...
array
:
A
):
Variants
<
A
[
number
]
>
;
export
function
variants
():
Variants
<
any
>
{
return
new
Variants
(
Array
.
isArray
(
arguments
[
0
])
?
arguments
[
0
]
:
Array
.
prototype
.
slice
.
call
(
arguments
));
}
...
...
fields/AutoComplete/state-machine.ts
View file @
876ca5fc
import
{
Err
,
AuthCtx
}
from
"~/context"
;
import
{
Cmd
,
noop
,
Eff
,
cmd
}
from
"
@bitmaster
/core"
;
import
{
Cmd
,
noop
,
Eff
,
cmd
}
from
"
../..
/core"
;
/**
...
...
fields/SelectField.tsx
View file @
876ca5fc
...
...
@@ -9,7 +9,7 @@ import Select, { SelectProps } from '@material-ui/core/Select';
import
{
AuthCtx
as
Ctx
}
from
'~/context'
;
import
{
Err
,
notifyError
}
from
'~/context'
;
import
*
as
Rx
from
'rxjs'
;
import
{
Eff
}
from
'
@bitmaster
/core'
;
import
{
Eff
}
from
'
..
/core'
;
// Источник для опций
...
...
@@ -25,7 +25,7 @@ export type AsyncSource<A> = {
// Props
export
type
Props
<
A
=
any
>
=
StandardProps
<
React
.
HTMLProps
<
HTMLDivElement
>
,
ClassKey
>
&
FieldProps
<
A
>
&
{
export
type
Props
<
A
=
any
>
=
StandardProps
<
React
.
HTMLProps
<
HTMLDivElement
>
,
ClassKey
,
'disabled'
>
&
FieldProps
<
A
>
&
{
selectProps
?:
Partial
<
SelectProps
>
;
source
:
Source
<
A
>
;
renderItem
?(
item
:
A
):
string
;
...
...
@@ -125,7 +125,7 @@ export default class SelectField<A = any> extends React.Component<Props<A>, Stat
const
valueIdx
=
options
.
findIndex
(
x
=>
predicate
(
x
,
value
!
));
return
(
<
Select
value=
{
valueIdx
}
onChange=
{
this
.
handleChange
}
className=
{
rootClass
}
disabled=
{
disabled
}
open=
{
open
}
onOpen=
{
this
.
handleOpen
}
onClose=
{
this
.
handleClose
}
>
<
Select
value=
{
valueIdx
}
onChange=
{
this
.
handleChange
}
className=
{
rootClass
}
disabled=
{
Boolean
(
disabled
)
}
open=
{
open
}
onOpen=
{
this
.
handleOpen
}
onClose=
{
this
.
handleClose
}
>
{
nullCase
&&
<
MenuItem
key=
"@null"
value=
{
-
1
}
>
{
nullCase
}
</
MenuItem
>
}
{
options
.
map
((
item
,
idx
)
=>
<
MenuItem
key=
{
idx
}
value=
{
idx
}
>
{
(
renderItem
||
String
)(
item
)
}
</
MenuItem
>)
}
</
Select
>
...
...
jsonapi/resources.ts
View file @
876ca5fc
...
...
@@ -4,7 +4,7 @@ import { Expr } from '../types';
import
*
as
decoders
from
'./decoders'
;
import
{
Decoder
,
Validation
}
from
'../decoder'
;
import
{
Left
,
Right
}
from
'../either'
;
import
{
Params
}
from
'../http'
;
import
{
Params
}
from
'../
core/
http'
;
/** Ресурс jsonapi */
...
...
@@ -85,7 +85,7 @@ export class JsonApiBase<A> {
case
'RelatedMany'
:
{
acc
.
fields
[
ty
]
=
acc
.
fields
[
ty
]
||
{};
acc
.
fields
[
ty
][
key
]
=
true
;
if
(
!
isEmpty
(
field
.
child
ren
.
desc
))
acc
.
include
[
path
.
concat
(
key
).
join
(
'.'
)]
=
true
;
if
(
!
isEmpty
(
field
.
child
.
desc
))
acc
.
include
[
path
.
concat
(
key
).
join
(
'.'
)]
=
true
;
for
(
let
k
in
field
.
child
.
desc
)
{
if
(
!
field
.
child
.
desc
.
hasOwnProperty
(
k
))
continue
;
go
(
acc
,
path
.
concat
(
key
),
field
.
child
.
type
,
k
,
field
.
child
.
desc
[
k
]);
...
...
utils/jsonapi/decoders.ts
View file @
876ca5fc
import
*
as
t
from
'../../decoder'
;
import
{
Decoder
,
decoder
,
required
,
optional
,
Validation
}
from
'../../decoder'
;
import
{
success
,
failure
}
from
'../../either'
;
/** tolarate numbers as strings */
export
const
stringOrNumber
:
Decoder
<
string
>
=
decoder
(
'stringOrNumber'
,
v
=>
typeof
(
v
)
===
'string'
||
typeof
(
v
)
===
'number'
?
success
(
String
(
v
))
:
failure
(
'not a string nor a number'
)
);
/** top level http://jsonapi.org/format/#document-top-level */
export
interface
Document
{
data
:
Resource
|
Resource
[]
|
null
|
undefined
;
meta
:
Record
<
string
,
JSON
>
|
null
;
included
:
Resource
[]
|
null
;
jsonapi
:
Jsonapi
|
null
;
links
:
{
[
key
:
string
]:
Link
}
|
null
;
}
/** a document, containing errors */
export
interface
Errors
{
errors
:
ErrorObject
[],
meta
:
Object
|
null
;
included
:
Resource
[]
|
null
;
jsonapi
:
Jsonapi
|
null
;
links
:
{
[
key
:
string
]:
Link
}
|
null
;
}
/** error obeject http://jsonapi.org/format/#error-objects */
export
interface
ErrorObject
{
id
:
string
|
null
;
links
:
{
[
s
:
string
]:
Link
}
|
null
;
status
:
string
|
null
;
code
:
string
|
null
;
title
:
string
|
null
;
detail
:
string
|
null
;
source
:
{
pointer
:
string
,
parameter
:
string
}
|
null
;
}
/** jsonapi resource http://jsonapi.org/format/#document-resource-objects */
export
interface
Resource
{
id
:
string
;
type
:
string
;
attributes
:
{
[
k
:
string
]:
any
}
|
null
;
relationships
:
{
[
k
:
string
]:
Relationship
}
|
null
;
meta
:
Object
|
null
;
}
/** link object http://jsonapi.org/format/#document-links */
export
type
Link
=
string
|
{
href
:
string
;
meta
:
Object
|
null
;
}
/** attributes http://jsonapi.org/format/#document-resource-object-attributes */
export
type
Attributes
=
Object
;
/** relationship http://jsonapi.org/format/#document-resource-object-relationships */
export
interface
Relationship
{
data
:
Linkage
;
meta
:
Object
|
null
;
links
:
{
[
k
:
string
]:
Link
}
|
null
;
}
/** linkage http://jsonapi.org/format/#document-resource-object-linkage */
export
type
Linkage
=
null
|
Identifier
|
Identifier
[]
/** resource identifier http://jsonapi.org/format/#document-resource-identifier-objects */
export
interface
Identifier
{
id
:
string
;
type
:
string
;
meta
:
object
|
null
;
}
/** jsonapi version http://jsonapi.org/format/#document-jsonapi-object */
export
interface
Jsonapi
{
version
:
string
;
}
/** identifier validation */
export
const
identifier
=
t
.
ap
(
required
(
'id'
,
stringOrNumber
),
required
(
'type'
,
t
.
string
),
optional
(
'meta'
,
tolerantDict
(
t
.
any
),
null
),
(
id
,
type
,
meta
)
=>
({
id
,
type
,
meta
})
);
/** linkage validation */
export
const
linkage
=
t
.
oneOf
(
t
.
null
,
identifier
,
t
.
array
(
identifier
));
/** link validation */
export
const
link
=
t
.
oneOf
(
t
.
string
,
t
.
ap
(
required
(
'href'
,
t
.
string
),
optional
(
'meta'
,
tolerantDict
(
t
.
any
),
null
),
(
href
,
meta
)
=>
({
href
,
meta
})
));
/** relationship validation */
export
const
relationship
=
t
.
ap
(
required
(
'data'
,
linkage
),
optional
(
'meta'
,
tolerantDict
(
t
.
any
),
null
),
optional
(
'links'
,
tolerantDict
(
link
),
null
),
(
data
,
meta
,
links
)
=>
({
data
,
meta
,
links
})
);
/** resource validation */
export
const
resource
:
Decoder
<
Resource
>
=
t
.
ap
(
required
(
'id'
,
stringOrNumber
),
required
(
'type'
,
t
.
string
),
optional
(
'attributes'
,
tolerantDict
(
t
.
any
),
null
),
optional
(
'relationships'
,
tolerantDict
(
relationship
),
null
),
optional
(
'meta'
,
tolerantDict
(
t
.
any
),
null
),
(
id
,
type
,
attributes
,
relationships
,
meta
)
=>
({
id
,
type
,
attributes
,
relationships
,
meta
})
);
/** jsonapi version validation */
export
const
jsonapi
=
required
(
'version'
,
t
.
string
).
map
(
version
=>
({
version
}));
/** document validation */
export
const
document
=
t
.
ap
(
optional
(
'data'
,
t
.
oneOf
(
resource
,
t
.
array
(
resource
),
t
.
null
),
undefined
),
optional
(
'meta'
,
tolerantDict
(
t
.
any
),
null
),
optional
(
'included'
,
t
.
array
(
resource
),
null
),
optional
(
'jsonapi'
,
jsonapi
,
null
),
optional
(
'links'
,
tolerantDict
(
link
),
null
),
(
data
,
meta
,
included
,
jsonapi
,
links
)
=>
({
data
,
meta
,
included
,
jsonapi
,
links
})
);
/** decode error object */
export
const
errorObject
=
t
.
ap
(
optional
(
'id'
,
t
.
string
,
null
),
optional
(
'links'
,
tolerantDict
(
link
),
null
),
optional
(
'status'
,
stringOrNumber
,
null
),
/* TODO: actualy can only be a string */
optional
(
'code'
,
t
.
string
,
null
),
optional
(
'title'
,
t
.
string
,
null
),
optional
(
'detail'
,
t
.
string
,
null
),
optional
(
'details'
,
t
.
string
,
null
),
/* HACK: due to TM quirks, this field doesn't appear in specification */
optional
(
'source'
,
t
.
record
({
pointer
:
t
.
string
,
parameter
:
t
.
string
}),
null
),
(
id
,
links
,
status
,
code
,
title
,
detail
,
details
,
source
)
=>
({
id
,
links
,
status
,
code
,
title
,
detail
:
detail
||
details
,
source
})
);
/** errors decoder */
export
const
errors
=
t
.
ap
(
required
(
'errors'
,
t
.
array
(
errorObject
)),
optional
(
'meta'
,
tolerantDict
(
t
.
any
),
null
),
optional
(
'jsonapi'
,
jsonapi
,
null
),
optional
(
'links'
,
tolerantDict
(
link
),
null
),
(
errors
,
meta
,
jsonapi
,
links
)
=>
({
errors
,
meta
,
jsonapi
,
links
})
);
/** validate attributes */
export
function
attributes
<
A
>
(
res
:
Resource
,
dec
:
Decoder
<
A
>
):
Validation
<
A
>
{
return
dec
.
validate
(
res
.
attributes
);
}
/** validate primary resource */
export
function
primary
(
doc
:
Document
):
Validation
<
Resource
>
{
return
Object
.
prototype
.
toString
.
call
(
doc
.
data
)
===
'[object Object]'
?
success
(
doc
.
data
as
Resource
)
:
failure
(
`jsonapi.primary: jsonapi document doesn't contain primary resorce`
);
}
/** validate resource collection */
export
function
collection
(
doc
:
Document
):
Validation
<
Resource
[]
>
{
return
Array
.
isArray
(
doc
.
data
)
?
success
(
doc
.
data
)
:
failure
(
'jsonapi.collection: jsonapi document doesn
\'
t contain collection of resorces'
);
}
/** meta */
export
function
meta
<
A
>
(
doc
:
Document
,
dec
:
Decoder
<
A
>
):
Validation
<
A
>
{
return
dec
.
validate
(
doc
.
meta
);
}
/** validate related resource */
export
function
related
(
doc
:
Document
,
res
:
Resource
,
name
:
string
):
Validation
<
Resource
>
{
if
(
doc
.
included
&&
res
.
relationships
&&
res
.
relationships
[
name
])
{
const
rel
:
Relationship
=
res
.
relationships
[
name
];
if
(
rel
.
data
!==
null
&&
!
Array
.
isArray
(
rel
.
data
))
{
const
{
id
,
type
}
=
rel
.
data
;
const
result
=
doc
.
included
.
find
(
a
=>
a
.
id
===
id
&&
a
.
type
===
type
);
return
result
?
success
(
result
)
:
failure
(
`jsonapi.related: cannot find related resource
${
name
}
-> '
${
type
}
', #
${
id
}
for '
${
res
.
type
}
', #
${
res
.
id
}
`
)
as
Validation
<
Resource
>
;
}
}
return
failure
(
`jsonapi.related: cannot find related resource
${
name
}
for '
${
res
.
type
}
', #
${
res
.
id
}
`
);
}
/** validate related resource (one-to-many) */
export
function
relatedCollection
(
doc
:
Document
,
res
:
Resource
,
name
:
string
):
Validation
<
Resource
[]
>
{
if
(
doc
.
included
&&
res
.
relationships
&&
res
.
relationships
[
name
])
{
const
rel
:
Relationship
=
res
.
relationships
[
name
];
if
(
Array
.
isArray
(
rel
.
data
))
{
const
acc
:
Resource
[]
=
[];
for
(
let
i
in
rel
.
data
)
{
const
{
id
,
type
}
=
rel
.
data
[
i
];
const
result
=
doc
.
included
.
find
(
a
=>
a
.
id
===
id
&&
a
.
type
===
type
);
if
(
result
)
{
acc
.
push
(
result
);
}
else
{
return
failure
(
`jsonapi.relatedCollection: cannot find related resource '
${
name
}
', id:
${
id
}
`
);
}
}
return
success
(
acc
);
}
}
return
failure
(
`jsonapi.relatedCollection: cannot find related resource '
${
name
}
' for '
${
res
.
type
}
', #
${
res
.
id
}
`
);
}
/** validate related resource (only identifier) */
export
function
relatedLinkage
(
res
:
Resource
,
name
:
string
):
Validation
<
Identifier
>
{
if
(
res
.
relationships
&&
res
.
relationships
[
name
])
{
const
rel
:
Relationship
=
res
.
relationships
[
name
];
return
rel
.
data
!==
null
&&
!
Array
.
isArray
(
rel
.
data
)
?
success
(
rel
.
data
)
:
failure
(
`jsonapi.relatedLinkage: cannot find relationship '
${
name
}
' for
${
res
.
type
}
#
${
res
.
id
}
`
);
}
return
failure
(
`relatedLinkage: trying to access relationship '
${
name
}
' on
${
res
.
type
}
#
${
res
.
id
}
: resource doesn't have relationships`
);
}
/** validate related resource collection (only identifiers) */
export
function
relatedLinkageCollection
(
res
:
Resource
,
name
:
string
):
Validation
<
Identifier
[]
>
{
if
(
res
.
relationships
&&
res
.
relationships
[
name
])
{
const
rel
:
Relationship
=
res
.
relationships
[
name
];
return
Array
.
isArray
(
rel
.
data
)
?
success
(
rel
.
data
)
:
failure
(
`relatedLinkageCollection: cannot find relationship '
${
name
}
' for
${
res
.
type
}
#
${
res
.
id
}
`
);
}
return
failure
(
`relatedLinkageCollection: trying to access relationship '
${
name
}
' on
${
res
.
type
}
#
${
res
.
id
}
: resource doesn't have relationships`
);
}
/** unlike `t.dict` this version ignores invalid values */
export
function
tolerantDict
<
A
>
(
d
:
Decoder
<
A
>
):
Decoder
<
Record
<
string
,
A
>>
{
return
decoder
(
'jsonapi.tolerantDict'
,
value
=>
{
if
(
Object
.
prototype
.
toString
.
call
(
value
)
!==
'[object Object]'
)
return
failure
(
'not an object'
);
const
output
=
{}
as
Record
<
string
,
A
>
;
for
(
let
key
in
value
)
{
if
(
!
value
.
hasOwnProperty
(
key
))
continue
;
const
ethr
=
d
.
validate
(
value
[
key
]);
switch
(
ethr
.
tag
)
{
case
'Left'
:
continue
;
case
'Right'
:
output
[
key
]
=
ethr
.
value
;
break
;
}
}
return
success
(
output
);
});
}
export
*
from
'../../jsonapi/decoders'
;
utils/jsonapi/index.ts
View file @
876ca5fc
import
*
as
decoders
from
'./decoders'
;
export
*
from
'./resources'
;
export
{
decoders
};
export
*
from
'../../jsonapi'
;
utils/jsonapi/resources.ts
View file @
876ca5fc
import
{
isEqual
,
isEmpty
}
from
'lodash'
;
import
{
decode
as
t
,
either
,
success
,
failure
}
from
'@bitmaster/core'
;
import
{
Expr
}
from
'@bitmaster/core/internal/expr'
;
import
*
as
decoders
from
'./decoders'
;
import
{
Decoder
,
Validation
}
from
'@bitmaster/core/decode'
;
import
{
Left
,
Right
}
from
'@bitmaster/core/either'
;
import
{
Params
}
from
'@bitmaster/core/http'
;
/** Ресурс jsonapi */
export
interface
Resource
<
T
extends
string
=
string
>
{
id
:
string
;
type
:
T
;
}
/** Вспомогательный тип для построения GET параметров */
export
interface
Flags
{
fields
:
Record
<
string
,
StringSet
>
;
include
:
StringSet
;
}
export
type
StringSet
=
Record
<
string
,
true
>
;
/** DSL для jsonapi ресурсов */
export
type
JsonApi
<
A
>
=
|
Attr
<
A
>
|
Related
<
A
>
|
RelatedMany
<
A
>
|
WithName
<
A
>
|
WithDefault
<
A
>
// Базовый класс для наследования методов
export
class
JsonApiBase
<
A
>
{
readonly
_A
:
A
;
/**
* Установка дефолтного значения в случае отсутствия поля
*/
withDefault
(
this
:
JsonApi
<
A
>
,
def
:
A
):
WithDefault
<
A
>
;
withDefault
<
B
extends
Expr
>
(
this
:
JsonApi
<
A
>
,
def
:
B
):
WithDefault
<
A
|
B
>
;
withDefault
<
B
extends
Expr
>
(
this
:
JsonApi
<
A
>
,
def
:
B
):
WithDefault
<
A
|
B
>
{
return
new
WithDefault
(
def
,
this
);
}
/**
* Создать `to-many` поле
*/
many
(
this
:
Related
<
A
>
):
RelatedMany
<
A
[]
>
{
return
new
RelatedMany
(
this
as
any
);
}
/**
* Сбор информации по полям ресурсов для построения GET запроса
*/
collectFlags
(
this
:
Related
<
A
>
):
Flags
{
const
output
:
Flags
=
{
fields
:
{},
include
:
{}
};
for
(
let
key
in
this
.
desc
)
{
if
(
!
this
.
desc
.
hasOwnProperty
(
key
))
continue
;
go
(
output
,
[],
this
.
type
,
key
,
this
.
desc
[
key
]);
}
return
output
;
function
go
(
acc
:
Flags
,
path
:
string
[],
ty
:
string
,
key
:
string
,
field
:
JsonApi
<
any
>
)
{
switch
(
field
.
tag
)
{
case
'Attr'
:
{
acc
.
fields
[
ty
]
=
acc
.
fields
[
ty
]
||
{};
acc
.
fields
[
ty
][
key
]
=
true
;
return
;
}
case
'Related'
:
{
acc
.
fields
[
ty
]
=
acc
.
fields
[
ty
]
||
{};
acc
.
fields
[
ty
][
key
]
=
true
;
if
(
!
isEmpty
(
field
.
desc
))
acc
.
include
[
path
.
concat
(
key
).
join
(
'.'
)]
=
true
;
for
(
let
k
in
field
.
desc
)
{
if
(
!
field
.
desc
.
hasOwnProperty
(
k
))
continue
;
go
(
acc
,
path
.
concat
(
key
),
field
.
type
,
k
,
field
.
desc
[
k
]);
}
return
;
}
case
'RelatedMany'
:
{
acc
.
fields
[
ty
]
=
acc
.
fields
[
ty
]
||
{};
acc
.
fields
[
ty
][
key
]
=
true
;
if
(
!
isEmpty
(
field
.
child
.
desc
))
acc
.
include
[
path
.
concat
(
key
).
join
(
'.'
)]
=
true
;
for
(
let
k
in
field
.
child
.
desc
)
{
if
(
!
field
.
child
.
desc
.
hasOwnProperty
(
k
))
continue
;
go
(
acc
,
path
.
concat
(
key
),
field
.
child
.
type
,
k
,
field
.
child
.
desc
[
k
]);
}
return
;
}
case
'WithDefault'
:
{
go
(
acc
,
path
,
ty
,
key
,
field
.
child
);
return
;
}
case
'WithName'
:
{
go
(
acc
,
path
,
ty
,
field
.
name
,
field
.
child
);
return
;
}
}
}
}
/**
* Построение GET запроса
*/
collectQuery
(
this
:
Related
<
A
>
):
Params
{
const
{
fields
,
include
}
=
this
.
collectFlags
();
const
output
=
{
include
:
Object
.
keys
(
include
).
join
(
','
)
||
undefined
,
}
as
Params
;
for
(
let
k
in
fields
)
{
output
[
`fields[
${
k
}
]`
]
=
Object
.
keys
(
fields
[
k
]).
join
(
','
)
||
undefined
;
}
return
output
;
}
/**
* Валидация jsonapi документа
*/
validate
(
this
:
Related
<
A
>
,
doc
:
decoders
.
Document
,
res
:
decoders
.
Resource
):
Validation
<
A
>
{
if
(
res
.
type
!==
this
.
type
)
return
failure
(
`invalid resource type:
${
res
.
type
}
, expected
${
this
.
type
}
`
);
const
output
:
Record
<
string
,
any
>
=
{
id
:
res
.
id
,
type
:
res
.
type
};
for
(
let
key
in
this
.
desc
)
{
if
(
!
this
.
desc
.
hasOwnProperty
(
key
))
continue
;
const
result
=
helperRec
(
key
,
this
.
desc
[
key
],
doc
,
res
);
if
(
result
instanceof
Left
)
return
result
;
output
[
key
]
=
result
[
'value'
];
}
return
success
(
output
as
A
);
function
helperRec
(
key
:
string
,
field
:
JsonApi
<
any
>
,
doc
:
decoders
.
Document
,
res
:
decoders
.
Resource
):
Validation
<
any
>
{
switch
(
field
.
tag
)
{
case
'Attr'
:
{
const
attrs
=
res
.
attributes
;
if
(
attrs
===
null
)
return
failure
(
`resource
${
res
.
type
}
#
${
res
.
id
}
doesn't have any attributes`
);
return
field
.
decoder
.
validate
(
attrs
[
key
]);
}
case
'Related'
:
{
if
(
isEmpty
(
field
.
desc
))
return
decoders
.
relatedLinkage
(
res
,
key
);
return
decoders
.
related
(
doc
,
res
,
key
).
chain
(
relatedRes
=>
field
.
validate
(
doc
,
relatedRes
));
}
case
'RelatedMany'
:
{
return
decoders
.
relatedCollection
(
doc
,
res
,
key
).
chain
(
rs
=>
either
.
traverse
(
rs
,
x
=>
field
.
child
.
validate
(
doc
,
x
)));
}
case
'WithDefault'
:
{
const
nested
=
field
.
child
;
switch
(
nested
.
tag
)
{
case
'Related'
:
return
decoders
.
related
(
doc
,
res
,
key
).
fold
(()
=>
success
(
field
.
defaultValue
),
relatedRes
=>
nested
.
validate
(
doc
,
relatedRes
));
case
'RelatedMany'
:
return
decoders
.
relatedCollection
(
doc
,
res
,
key
).
fold
(()
=>
success
(
field
.
defaultValue
),
rs
=>
either
.
traverse
(
rs
,
x
=>
nested
.
child
.
validate
(
doc
,
x
)));
}
const
result
=
helperRec
(
key
,
field
.
child
,
doc
,
res
);
if
(
result
instanceof
Right
)
return
result
;
return
success
(
field
.
defaultValue
);
}
case
'WithName'
:
{
return
helperRec
(
field
.
name
,
field
.
child
,
doc
,
res
)
}
}
}
}
/**
* Построение содержимого POST запроса
*/
post
(
this
:
Related
<
A
>
,
resource
:
A
):
object
{
const
data
:
Record
<
string
,
any
>
=
{
type
:
this
.
type
};
const
attributes
:
Record
<
string
,
any
>
=
{};
const
relationships
:
Record
<
string
,
any
>
=
{};
for
(
let
key
in
this
.
desc
)
{
if
(
!
this
.
desc
.
hasOwnProperty
(
key
))
continue
;
postRec
(
key
,
key
,
this
.
desc
[
key
]);
}
if
(
!
isEmpty
(
attributes
))
data
.
attributes
=
attributes
;
if
(
!
isEmpty
(
relationships
))
data
.
relationships
=
relationships
;
return
{
data
};
function
postRec
(
ikey
:
string
,
okey
:
string
,
field
:
JsonApi
<
any
>
)
{
switch
(
field
.
tag
)
{
case
'Attr'
:
attributes
[
okey
]
=
resource
[
ikey
];
return
;
case
'Related'
:
relationships
[
okey
]
=
{
data
:
{
type
:
resource
[
ikey
].
type
,
id
:
resource
[
ikey
].
id
}
};
return
;
case
'RelatedMany'
:
relationships
[
okey
]
=
{
data
:
resource
[
ikey
].
map
(
x
=>
({
type
:
x
.
type
,
id
:
x
.
id
}))
};
return
;
case
'WithDefault'
:
if
(
!
isEqual
(
resource
[
ikey
],
field
.
defaultValue
))
postRec
(
ikey
,
okey
,
field
.
child
);
return
;
case
'WithName'
:
postRec
(
ikey
,
field
.
name
,
field
.
child
);
return
;
}
}
}
/**
* Построение содержимого PATCH запроса
*/
patch
(
this
:
Related
<
A
>
,
id
:
string
,
patch
:
Partial
<
A
>
):
object
{
const
data
:
Record
<
string
,
any
>
=
{
type
:
this
.
type
,
id
};
const
attributes
:
Record
<
string
,
any
>
=
{};
const
relationships
:
Record
<
string
,
any
>
=
{};
for
(
let
key
in
patch
)
{
if
(
!
this
.
desc
.
hasOwnProperty
(
key
))
continue
;
patchRec
(
key
,
key
,
this
.
desc
[
key
]);
}
if
(
!
isEmpty
(
attributes
))
data
.
attributes
=
attributes
;
if
(
!
isEmpty
(
relationships
))
data
.
relationships
=
relationships
;
return
{
data
};
function
patchRec
(
ikey
:
string
,
okey
:
string
,
field
:
JsonApi
<
any
>
)
{
switch
(
field
.
tag
)
{
case
'Attr'
:
attributes
[
okey
]
=
patch
[
ikey
];
return
;
case
'Related'
:
relationships
[
okey
]
=
{
data
:
{
type
:
patch
[
ikey
].
type
,
id
:
patch
[
ikey
].
id
}
};
return
;
case
'RelatedMany'
:
relationships
[
okey
]
=
{
data
:
patch
[
ikey
].
map
(
x
=>
({
type
:
x
.
type
,
id
:
x
.
id
}))
};
return
;
case
'WithDefault'
:
if
(
isEqual
(
patch
[
ikey
],
field
.
defaultValue
))
{
switch
(
field
.
child
.
tag
)
{
case
'Attr'
:
attributes
[
okey
]
=
field
.
defaultValue
;
return
;
case
'Related'
:
relationships
[
okey
]
=
{
data
:
null
};
return
;
case
'RelatedMany'
:
relationships
[
okey
]
=
{
data
:
[]
};
return
;
default
:
console
.
warn
(
`invalid tag nested inside WithDefault
${
field
.
child
.
tag
}
`
);
return
;
}
}
patchRec
(
ikey
,
okey
,
field
.
child
);
return
;
case
'WithName'
:
patchRec
(
ikey
,
field
.
name
,
field
.
child
);
return
;
}
}
}
/**
* Декодер для primary документа
*/
primaryDecoder
(
this
:
Related
<
A
>
):
Decoder
<
A
>
{
return
t
.
decoder
(
'primary'
,
val
=>
decoders
.
document
.
validate
(
val
).
chain
(
doc
=>
decoders
.
primary
(
doc
).
chain
(
res
=>
this
.
validate
(
doc
,
res
))
));
}
/**
* Декодер для коллекции документов
*/
collectionDecoder
(
this
:
Related
<
A
>
):
Decoder
<
A
[]
>
{
return
t
.
decoder
(
'collection'
,
val
=>
decoders
.
document
.
validate
(
val
).
chain
(
doc
=>
ensureCollection
(
doc
).
chain
(
rs
=>
either
.
traverse
(
rs
,
res
=>
this
.
validate
(
doc
,
res
))
))
);
function
ensureCollection
(
doc
:
decoders
.
Document
):
Validation
<
Array
<
decoders
.
Resource
>>
{
return
Array
.
isArray
(
doc
.
data
)
?
success
(
doc
.
data
)
:
failure
(
`jsonapi document doesn't contain collection of resorces`
);
}
}
/**
* Расширение набора полей
*/
extend
<
R
extends
ResourceRecord
>
(
this
:
Related
<
A
>
,
desc
:
R
):
Related
<
A
&
{
[
K
in
keyof
R
]:
R
[
K
][
'_A'
]
}
>
{
const
replaceDecoderWithAttr
=
{
...
this
.
desc
}
as
Related
<
any
>
[
'desc'
];
if
(
desc
)
for
(
let
k
in
desc
)
{
if
(
!
desc
.
hasOwnProperty
(
k
))
continue
;
if
(
desc
[
k
]
instanceof
t
.
DecoderBase
)
replaceDecoderWithAttr
[
k
]
=
new
Attr
(
desc
[
k
]
as
any
);
else
replaceDecoderWithAttr
[
k
]
=
desc
[
k
]
as
any
;
}
return
new
Related
(
this
.
type
,
replaceDecoderWithAttr
);
}
/** Переименование поля */
withName
(
name
:
string
):
WithName
<
A
>
{
return
new
WithName
(
name
,
this
as
any
);
}
}
/**
* Атрибут ресурса
*/
export
class
Attr
<
A
>
extends
JsonApiBase
<
A
>
{
readonly
tag
:
'Attr'
=
'Attr'
;
constructor
(
readonly
decoder
:
Decoder
<
A
>
,
)
{
super
();
}
}
/**
* Связанный ресурс
*/
export
class
Related
<
A
>
extends
JsonApiBase
<
A
>
{
readonly
tag
:
'Related'
=
'Related'
;
constructor
(
readonly
type
:
string
,
readonly
desc
:
Record
<
string
,
JsonApi
<
any
>>
,
)
{
super
();
}
}
/**
* Связанный ресурс (to-many)
*/
export
class
RelatedMany
<
A
>
extends
JsonApiBase
<
A
>
{
readonly
tag
:
'RelatedMany'
=
'RelatedMany'
;
constructor
(
readonly
child
:
Related
<
any
>
,
// A[number]
)
{
super
();
}
}
/**
* Поле с fallback значением
*/
export
class
WithDefault
<
A
>
extends
JsonApiBase
<
A
>
{
readonly
tag
:
'WithDefault'
=
'WithDefault'
;
constructor
(
readonly
defaultValue
:
A
,
readonly
child
:
JsonApi
<
any
>
,
)
{
super
();
}
}
/**
* Поле с переименованием
*/
export
class
WithName
<
A
>
extends
JsonApiBase
<
A
>
{
readonly
tag
:
'WithName'
=
'WithName'
;
constructor
(
readonly
name
:
string
,
readonly
child
:
JsonApi
<
any
>
,
)
{
super
();
}
}
// Тип аргумента для `resource`
export
type
ResourceRecord
=
Record
<
string
,
Decoder
<
any
>|
JsonApi
<
any
>>
;
/**
* Построение инстанса для `Related`
*/
export
function
resource
<
R
extends
ResourceRecord
,
T
extends
string
>
(
type
:
T
):
Related
<
Resource
<
T
>>
;
export
function
resource
<
R
extends
ResourceRecord
,
T
extends
string
>
(
type
:
T
,
desc
:
R
):
Related
<
Resource
<
T
>
&
{
[
K
in
keyof
R
]:
R
[
K
][
'_A'
]
}
>
;
export
function
resource
<
R
extends
ResourceRecord
,
T
extends
string
>
(
type
:
T
,
desc
?:
R
):
Related
<
any
>
{
const
replaceDecoderWithAttr
=
{}
as
Related
<
any
>
[
'desc'
];
if
(
desc
)
for
(
let
k
in
desc
)
{
if
(
!
desc
.
hasOwnProperty
(
k
))
continue
;
if
(
desc
[
k
]
instanceof
t
.
DecoderBase
)
replaceDecoderWithAttr
[
k
]
=
new
Attr
(
desc
[
k
]
as
any
);
else
replaceDecoderWithAttr
[
k
]
=
desc
[
k
]
as
any
;
}
return
new
Related
(
type
,
replaceDecoderWithAttr
);
}
/** Конструцтор атрибута */
export
function
attr
<
A
>
(
decoder
:
Decoder
<
A
>
):
Attr
<
A
>
{
return
new
Attr
(
decoder
);
}
export
*
from
'../../jsonapi/resources'
;
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment