Add explicit CRDT edit contracts and structured query values
This commit is contained in:
@@ -107,6 +107,7 @@ valueMember
|
|||||||
valueMemberOperation
|
valueMemberOperation
|
||||||
: GET ID stringLiteral executionClause? SEMI
|
: GET ID stringLiteral executionClause? SEMI
|
||||||
| SET ID stringLiteral executionClause? SEMI
|
| SET ID stringLiteral executionClause? SEMI
|
||||||
|
| EDIT ID stringLiteral executionClause? SEMI
|
||||||
| WATCH START ID stringLiteral STOP ID stringLiteral SEMI
|
| WATCH START ID stringLiteral STOP ID stringLiteral SEMI
|
||||||
;
|
;
|
||||||
|
|
||||||
@@ -234,6 +235,7 @@ primitiveList
|
|||||||
primitive
|
primitive
|
||||||
: READ
|
: READ
|
||||||
| WRITE
|
| WRITE
|
||||||
|
| EDIT
|
||||||
| RESOLVE
|
| RESOLVE
|
||||||
| CONNECT
|
| CONNECT
|
||||||
| DISCONNECT
|
| DISCONNECT
|
||||||
@@ -316,6 +318,7 @@ operationName
|
|||||||
| CALL
|
| CALL
|
||||||
| GET
|
| GET
|
||||||
| SET
|
| SET
|
||||||
|
| EDIT
|
||||||
| RESOLVE
|
| RESOLVE
|
||||||
| CONNECT
|
| CONNECT
|
||||||
| DISCONNECT
|
| DISCONNECT
|
||||||
@@ -335,6 +338,7 @@ operationProvider
|
|||||||
statePrimitive
|
statePrimitive
|
||||||
: READ
|
: READ
|
||||||
| WRITE
|
| WRITE
|
||||||
|
| EDIT
|
||||||
| WATCH_START
|
| WATCH_START
|
||||||
| WATCH_STOP
|
| WATCH_STOP
|
||||||
;
|
;
|
||||||
@@ -426,6 +430,21 @@ jsonArray
|
|||||||
|
|
||||||
identifier
|
identifier
|
||||||
: IDENTIFIER
|
: IDENTIFIER
|
||||||
|
| START
|
||||||
|
| STOP
|
||||||
|
| EDIT
|
||||||
|
| CREATION
|
||||||
|
| INITIALIZE
|
||||||
|
| EXECUTION
|
||||||
|
| SNAPSHOT
|
||||||
|
| TRANSACTION
|
||||||
|
| REMOTE
|
||||||
|
| TARGETS
|
||||||
|
| PORTABLE
|
||||||
|
| REPLICA
|
||||||
|
| LOCAL
|
||||||
|
| FROM
|
||||||
|
| REGISTER
|
||||||
| INTERFACES
|
| INTERFACES
|
||||||
| REFLECTION
|
| REFLECTION
|
||||||
| SOURCE
|
| SOURCE
|
||||||
@@ -449,6 +468,7 @@ stringLiteral
|
|||||||
: STRING_LITERAL
|
: STRING_LITERAL
|
||||||
;
|
;
|
||||||
|
|
||||||
|
EDIT: 'edit';
|
||||||
CREATION: 'creation';
|
CREATION: 'creation';
|
||||||
INITIALIZE: 'initialize';
|
INITIALIZE: 'initialize';
|
||||||
WORKSPACE: 'workspace';
|
WORKSPACE: 'workspace';
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ message QueryRelationPath {
|
|||||||
message QueryFieldOperand {
|
message QueryFieldOperand {
|
||||||
string interface_revision_id = 1;
|
string interface_revision_id = 1;
|
||||||
string member_id = 2;
|
string member_id = 2;
|
||||||
|
repeated string value_path = 3;
|
||||||
}
|
}
|
||||||
message QueryExpression {
|
message QueryExpression {
|
||||||
repeated QueryPathStep path = 1;
|
repeated QueryPathStep path = 1;
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ export const generateClientContracts = (
|
|||||||
return qualified
|
return qualified
|
||||||
? `CapabilityReference<${JSON.stringify(value.expectation.kind === "atom" ? `atom:${value.expectation.atomId}` : `interface:${value.expectation.interfaceRevisionId}`)}>`
|
? `CapabilityReference<${JSON.stringify(value.expectation.kind === "atom" ? `atom:${value.expectation.atomId}` : `interface:${value.expectation.interfaceRevisionId}`)}>`
|
||||||
: `{readonly $quixosRef: string}`;
|
: `{readonly $quixosRef: string}`;
|
||||||
|
case "change":
|
||||||
|
throw new Error("CRDT change values require the prepared-change codec and runtime (LF-19)");
|
||||||
case "optional":
|
case "optional":
|
||||||
return `(${type(value.value)} | null)`;
|
return `(${type(value.value)} | null)`;
|
||||||
case "list":
|
case "list":
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ export const genericImplementationType = (
|
|||||||
return `QxObjectRef<${target(type.expectation, scope)}>`;
|
return `QxObjectRef<${target(type.expectation, scope)}>`;
|
||||||
case "record":
|
case "record":
|
||||||
return object(Object.entries(type.fields).map(([name, field]) => [name, value(field, scope, depth + 1)]));
|
return object(Object.entries(type.fields).map(([name, field]) => [name, value(field, scope, depth + 1)]));
|
||||||
|
case "change":
|
||||||
|
throw new Error("CRDT change values require the prepared-change codec and runtime (LF-19)");
|
||||||
case "optional":
|
case "optional":
|
||||||
return `(${value(type.value, scope, depth + 1)} | null)`;
|
return `(${value(type.value, scope, depth + 1)} | null)`;
|
||||||
case "list":
|
case "list":
|
||||||
|
|||||||
@@ -122,6 +122,8 @@ export const generateTypeScriptBindings = (
|
|||||||
}[value.name];
|
}[value.name];
|
||||||
case "object-ref":
|
case "object-ref":
|
||||||
return `QxObjectRef<${q(value.expectation.kind === "atom" ? `atom:${value.expectation.atomId}` : `interface:${value.expectation.interfaceRevisionId}`)}>`;
|
return `QxObjectRef<${q(value.expectation.kind === "atom" ? `atom:${value.expectation.atomId}` : `interface:${value.expectation.interfaceRevisionId}`)}>`;
|
||||||
|
case "change":
|
||||||
|
throw new Error("CRDT change values require the prepared-change codec and runtime (LF-19)");
|
||||||
case "optional":
|
case "optional":
|
||||||
return `(${type(value.value)} | null)`;
|
return `(${type(value.value)} | null)`;
|
||||||
case "list":
|
case "list":
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ export function generateReactBindings(
|
|||||||
uint32: "number",
|
uint32: "number",
|
||||||
double: "number",
|
double: "number",
|
||||||
}[value.name];
|
}[value.name];
|
||||||
|
case "change":
|
||||||
|
throw new Error("CRDT change values require the prepared-change codec and runtime (LF-19)");
|
||||||
case "optional":
|
case "optional":
|
||||||
return `(${type(value.value)} | null)`;
|
return `(${type(value.value)} | null)`;
|
||||||
case "list":
|
case "list":
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,283 +1,285 @@
|
|||||||
CREATION=1
|
EDIT=1
|
||||||
INITIALIZE=2
|
CREATION=2
|
||||||
WORKSPACE=3
|
INITIALIZE=3
|
||||||
EXECUTION=4
|
WORKSPACE=4
|
||||||
SNAPSHOT=5
|
EXECUTION=5
|
||||||
TRANSACTION=6
|
SNAPSHOT=6
|
||||||
REMOTE=7
|
TRANSACTION=7
|
||||||
TARGETS=8
|
REMOTE=8
|
||||||
PORTABLE=9
|
TARGETS=9
|
||||||
REPLICA=10
|
PORTABLE=10
|
||||||
LOCAL=11
|
REPLICA=11
|
||||||
FROM=12
|
LOCAL=12
|
||||||
RESULT_BYTES=13
|
FROM=13
|
||||||
RPC_CALLS=14
|
RESULT_BYTES=14
|
||||||
DEADLINE_MS=15
|
RPC_CALLS=15
|
||||||
QUERY=16
|
DEADLINE_MS=16
|
||||||
REFLECTION=17
|
QUERY=17
|
||||||
SPECIALIZE=18
|
REFLECTION=18
|
||||||
ROOT=19
|
SPECIALIZE=19
|
||||||
DOCUMENT=20
|
ROOT=20
|
||||||
FRAGMENTS=21
|
DOCUMENT=21
|
||||||
VIEW=22
|
FRAGMENTS=22
|
||||||
MAX=23
|
VIEW=23
|
||||||
ALLOW=24
|
MAX=24
|
||||||
POLL=25
|
ALLOW=25
|
||||||
QUERYABLE=26
|
POLL=26
|
||||||
RPC=27
|
QUERYABLE=27
|
||||||
QUERY_REASON=28
|
RPC=28
|
||||||
TYPE=29
|
QUERY_REASON=29
|
||||||
OBJECT=30
|
TYPE=30
|
||||||
STORABLE=31
|
OBJECT=31
|
||||||
IMPLEMENTS=32
|
STORABLE=32
|
||||||
REF=33
|
IMPLEMENTS=33
|
||||||
FRAGMENT=34
|
REF=34
|
||||||
IMPORT=35
|
FRAGMENT=35
|
||||||
EXTERNAL=36
|
IMPORT=36
|
||||||
ATOM=37
|
EXTERNAL=37
|
||||||
INTERFACE=38
|
ATOM=38
|
||||||
INTERFACES=39
|
INTERFACE=39
|
||||||
PACKAGE=40
|
INTERFACES=40
|
||||||
VALUE=41
|
PACKAGE=41
|
||||||
RELATION=42
|
VALUE=42
|
||||||
OPERATION=43
|
RELATION=43
|
||||||
FUNCTION=44
|
OPERATION=44
|
||||||
CONSTRUCTOR=45
|
FUNCTION=45
|
||||||
CONSTRUCTS=46
|
CONSTRUCTOR=46
|
||||||
INPUT=47
|
CONSTRUCTS=47
|
||||||
CONFORM=48
|
INPUT=48
|
||||||
AS=49
|
CONFORM=49
|
||||||
BIND=50
|
AS=50
|
||||||
STATIC=51
|
BIND=51
|
||||||
TO=52
|
STATIC=52
|
||||||
PRIVATE=53
|
TO=53
|
||||||
SHARED=54
|
PRIVATE=54
|
||||||
STATE=55
|
SHARED=55
|
||||||
EDGE=56
|
STATE=56
|
||||||
PROJECTION=57
|
EDGE=57
|
||||||
WITH=58
|
PROJECTION=58
|
||||||
USING=59
|
WITH=59
|
||||||
VIA=60
|
USING=60
|
||||||
MATERIALIZE=61
|
VIA=61
|
||||||
IF=62
|
MATERIALIZE=62
|
||||||
ABSENT=63
|
IF=63
|
||||||
ON=64
|
ABSENT=64
|
||||||
POLICY=65
|
ON=65
|
||||||
DEFAULT=66
|
POLICY=66
|
||||||
SOURCE=67
|
DEFAULT=67
|
||||||
REPOSITORY=68
|
SOURCE=68
|
||||||
COMMIT=69
|
REPOSITORY=69
|
||||||
REVISION=70
|
COMMIT=70
|
||||||
SEMANTIC_MAJOR=71
|
REVISION=71
|
||||||
ON_DELETE=72
|
SEMANTIC_MAJOR=72
|
||||||
RETAIN_OTHER=73
|
ON_DELETE=73
|
||||||
KEYED=74
|
RETAIN_OTHER=74
|
||||||
PUBLIC_TRAVERSAL=75
|
KEYED=75
|
||||||
ID=76
|
PUBLIC_TRAVERSAL=76
|
||||||
DOC=77
|
ID=77
|
||||||
MODE=78
|
DOC=78
|
||||||
EMITS=79
|
MODE=79
|
||||||
RECEIVER=80
|
EMITS=80
|
||||||
REQUIRES=81
|
RECEIVER=81
|
||||||
ANY=82
|
REQUIRES=82
|
||||||
GET=83
|
ANY=83
|
||||||
SET=84
|
GET=84
|
||||||
WATCH=85
|
SET=85
|
||||||
START=86
|
WATCH=86
|
||||||
STOP=87
|
START=87
|
||||||
READ=88
|
STOP=88
|
||||||
WRITE=89
|
READ=89
|
||||||
RESOLVE=90
|
WRITE=90
|
||||||
CONNECT=91
|
RESOLVE=91
|
||||||
DISCONNECT=92
|
CONNECT=92
|
||||||
CALL=93
|
DISCONNECT=93
|
||||||
WATCH_START=94
|
CALL=94
|
||||||
WATCH_STOP=95
|
WATCH_START=95
|
||||||
SUBSCRIBE=96
|
WATCH_STOP=96
|
||||||
UNSUBSCRIBE=97
|
SUBSCRIBE=97
|
||||||
REGISTER=98
|
UNSUBSCRIBE=98
|
||||||
CRDT=99
|
REGISTER=99
|
||||||
OPTIONAL_ONE=100
|
CRDT=100
|
||||||
EXACTLY_ONE=101
|
OPTIONAL_ONE=101
|
||||||
MANY_UNIQUE=102
|
EXACTLY_ONE=102
|
||||||
MANY=103
|
MANY_UNIQUE=103
|
||||||
ORDERED=104
|
MANY=104
|
||||||
UNIT=105
|
ORDERED=105
|
||||||
WATCH_HANDLE=106
|
UNIT=106
|
||||||
MESSAGE=107
|
WATCH_HANDLE=107
|
||||||
ATOM_REF=108
|
MESSAGE=108
|
||||||
INTERFACE_REF=109
|
ATOM_REF=109
|
||||||
OPTIONAL=110
|
INTERFACE_REF=110
|
||||||
LIST=111
|
OPTIONAL=111
|
||||||
RECORD=112
|
LIST=112
|
||||||
BOOL=113
|
RECORD=113
|
||||||
BYTES=114
|
BOOL=114
|
||||||
DOUBLE=115
|
BYTES=115
|
||||||
INT32=116
|
DOUBLE=116
|
||||||
INT64=117
|
INT32=117
|
||||||
STRING=118
|
INT64=118
|
||||||
UINT32=119
|
STRING=119
|
||||||
UINT64=120
|
UINT32=120
|
||||||
TRUE=121
|
UINT64=121
|
||||||
FALSE=122
|
TRUE=122
|
||||||
NULL=123
|
FALSE=123
|
||||||
ARROW=124
|
NULL=124
|
||||||
COLON=125
|
ARROW=125
|
||||||
SEMI=126
|
COLON=126
|
||||||
COMMA=127
|
SEMI=127
|
||||||
DOT=128
|
COMMA=128
|
||||||
LBRACE=129
|
DOT=129
|
||||||
RBRACE=130
|
LBRACE=130
|
||||||
LBRACK=131
|
RBRACE=131
|
||||||
RBRACK=132
|
LBRACK=132
|
||||||
LPAREN=133
|
RBRACK=133
|
||||||
RPAREN=134
|
LPAREN=134
|
||||||
LT=135
|
RPAREN=135
|
||||||
GT=136
|
LT=136
|
||||||
AMP=137
|
GT=137
|
||||||
EQUAL=138
|
AMP=138
|
||||||
INTEGER=139
|
EQUAL=139
|
||||||
JSON_NUMBER=140
|
INTEGER=140
|
||||||
IDENTIFIER=141
|
JSON_NUMBER=141
|
||||||
STRING_LITERAL=142
|
IDENTIFIER=142
|
||||||
LINE_COMMENT=143
|
STRING_LITERAL=143
|
||||||
BLOCK_COMMENT=144
|
LINE_COMMENT=144
|
||||||
WS=145
|
BLOCK_COMMENT=145
|
||||||
'creation'=1
|
WS=146
|
||||||
'initialize'=2
|
'edit'=1
|
||||||
'workspace'=3
|
'creation'=2
|
||||||
'execution'=4
|
'initialize'=3
|
||||||
'snapshot'=5
|
'workspace'=4
|
||||||
'transaction'=6
|
'execution'=5
|
||||||
'remote'=7
|
'snapshot'=6
|
||||||
'targets'=8
|
'transaction'=7
|
||||||
'portable'=9
|
'remote'=8
|
||||||
'replica'=10
|
'targets'=9
|
||||||
'local'=11
|
'portable'=10
|
||||||
'from'=12
|
'replica'=11
|
||||||
'result-bytes'=13
|
'local'=12
|
||||||
'rpc-calls'=14
|
'from'=13
|
||||||
'deadline-ms'=15
|
'result-bytes'=14
|
||||||
'query'=16
|
'rpc-calls'=15
|
||||||
'reflection'=17
|
'deadline-ms'=16
|
||||||
'specialize'=18
|
'query'=17
|
||||||
'root'=19
|
'reflection'=18
|
||||||
'document'=20
|
'specialize'=19
|
||||||
'fragments'=21
|
'root'=20
|
||||||
'view'=22
|
'document'=21
|
||||||
'max'=23
|
'fragments'=22
|
||||||
'allow'=24
|
'view'=23
|
||||||
'poll'=25
|
'max'=24
|
||||||
'queryable'=26
|
'allow'=25
|
||||||
'rpc'=27
|
'poll'=26
|
||||||
'query-reason'=28
|
'queryable'=27
|
||||||
'type'=29
|
'rpc'=28
|
||||||
'object'=30
|
'query-reason'=29
|
||||||
'storable'=31
|
'type'=30
|
||||||
'implements'=32
|
'object'=31
|
||||||
'ref'=33
|
'storable'=32
|
||||||
'fragment'=34
|
'implements'=33
|
||||||
'import'=35
|
'ref'=34
|
||||||
'external'=36
|
'fragment'=35
|
||||||
'atom'=37
|
'import'=36
|
||||||
'interface'=38
|
'external'=37
|
||||||
'interfaces'=39
|
'atom'=38
|
||||||
'package'=40
|
'interface'=39
|
||||||
'value'=41
|
'interfaces'=40
|
||||||
'relation'=42
|
'package'=41
|
||||||
'operation'=43
|
'value'=42
|
||||||
'function'=44
|
'relation'=43
|
||||||
'constructor'=45
|
'operation'=44
|
||||||
'constructs'=46
|
'function'=45
|
||||||
'input'=47
|
'constructor'=46
|
||||||
'conform'=48
|
'constructs'=47
|
||||||
'as'=49
|
'input'=48
|
||||||
'bind'=50
|
'conform'=49
|
||||||
'static'=51
|
'as'=50
|
||||||
'to'=52
|
'bind'=51
|
||||||
'private'=53
|
'static'=52
|
||||||
'shared'=54
|
'to'=53
|
||||||
'state'=55
|
'private'=54
|
||||||
'edge'=56
|
'shared'=55
|
||||||
'projection'=57
|
'state'=56
|
||||||
'with'=58
|
'edge'=57
|
||||||
'using'=59
|
'projection'=58
|
||||||
'via'=60
|
'with'=59
|
||||||
'materialize'=61
|
'using'=60
|
||||||
'if'=62
|
'via'=61
|
||||||
'absent'=63
|
'materialize'=62
|
||||||
'on'=64
|
'if'=63
|
||||||
'policy'=65
|
'absent'=64
|
||||||
'default'=66
|
'on'=65
|
||||||
'source'=67
|
'policy'=66
|
||||||
'repository'=68
|
'default'=67
|
||||||
'commit'=69
|
'source'=68
|
||||||
'revision'=70
|
'repository'=69
|
||||||
'semantic-major'=71
|
'commit'=70
|
||||||
'on-delete'=72
|
'revision'=71
|
||||||
'retain-other'=73
|
'semantic-major'=72
|
||||||
'keyed'=74
|
'on-delete'=73
|
||||||
'public-traversal'=75
|
'retain-other'=74
|
||||||
'id'=76
|
'keyed'=75
|
||||||
'doc'=77
|
'public-traversal'=76
|
||||||
'mode'=78
|
'id'=77
|
||||||
'emits'=79
|
'doc'=78
|
||||||
'receiver'=80
|
'mode'=79
|
||||||
'requires'=81
|
'emits'=80
|
||||||
'any'=82
|
'receiver'=81
|
||||||
'get'=83
|
'requires'=82
|
||||||
'set'=84
|
'any'=83
|
||||||
'watch'=85
|
'get'=84
|
||||||
'start'=86
|
'set'=85
|
||||||
'stop'=87
|
'watch'=86
|
||||||
'read'=88
|
'start'=87
|
||||||
'write'=89
|
'stop'=88
|
||||||
'resolve'=90
|
'read'=89
|
||||||
'connect'=91
|
'write'=90
|
||||||
'disconnect'=92
|
'resolve'=91
|
||||||
'call'=93
|
'connect'=92
|
||||||
'watch-start'=94
|
'disconnect'=93
|
||||||
'watch-stop'=95
|
'call'=94
|
||||||
'subscribe'=96
|
'watch-start'=95
|
||||||
'unsubscribe'=97
|
'watch-stop'=96
|
||||||
'register'=98
|
'subscribe'=97
|
||||||
'crdt'=99
|
'unsubscribe'=98
|
||||||
'optional-one'=100
|
'register'=99
|
||||||
'exactly-one'=101
|
'crdt'=100
|
||||||
'many-unique'=102
|
'optional-one'=101
|
||||||
'many'=103
|
'exactly-one'=102
|
||||||
'ordered'=104
|
'many-unique'=103
|
||||||
'unit'=105
|
'many'=104
|
||||||
'watch-handle'=106
|
'ordered'=105
|
||||||
'message'=107
|
'unit'=106
|
||||||
'atom-ref'=108
|
'watch-handle'=107
|
||||||
'interface-ref'=109
|
'message'=108
|
||||||
'optional'=110
|
'atom-ref'=109
|
||||||
'list'=111
|
'interface-ref'=110
|
||||||
'record'=112
|
'optional'=111
|
||||||
'bool'=113
|
'list'=112
|
||||||
'bytes'=114
|
'record'=113
|
||||||
'double'=115
|
'bool'=114
|
||||||
'int32'=116
|
'bytes'=115
|
||||||
'int64'=117
|
'double'=116
|
||||||
'string'=118
|
'int32'=117
|
||||||
'uint32'=119
|
'int64'=118
|
||||||
'uint64'=120
|
'string'=119
|
||||||
'true'=121
|
'uint32'=120
|
||||||
'false'=122
|
'uint64'=121
|
||||||
'null'=123
|
'true'=122
|
||||||
'->'=124
|
'false'=123
|
||||||
':'=125
|
'null'=124
|
||||||
';'=126
|
'->'=125
|
||||||
','=127
|
':'=126
|
||||||
'.'=128
|
';'=127
|
||||||
'{'=129
|
','=128
|
||||||
'}'=130
|
'.'=129
|
||||||
'['=131
|
'{'=130
|
||||||
']'=132
|
'}'=131
|
||||||
'('=133
|
'['=132
|
||||||
')'=134
|
']'=133
|
||||||
'<'=135
|
'('=134
|
||||||
'>'=136
|
')'=135
|
||||||
'&'=137
|
'<'=136
|
||||||
'='=138
|
'>'=137
|
||||||
|
'&'=138
|
||||||
|
'='=139
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,283 +1,285 @@
|
|||||||
CREATION=1
|
EDIT=1
|
||||||
INITIALIZE=2
|
CREATION=2
|
||||||
WORKSPACE=3
|
INITIALIZE=3
|
||||||
EXECUTION=4
|
WORKSPACE=4
|
||||||
SNAPSHOT=5
|
EXECUTION=5
|
||||||
TRANSACTION=6
|
SNAPSHOT=6
|
||||||
REMOTE=7
|
TRANSACTION=7
|
||||||
TARGETS=8
|
REMOTE=8
|
||||||
PORTABLE=9
|
TARGETS=9
|
||||||
REPLICA=10
|
PORTABLE=10
|
||||||
LOCAL=11
|
REPLICA=11
|
||||||
FROM=12
|
LOCAL=12
|
||||||
RESULT_BYTES=13
|
FROM=13
|
||||||
RPC_CALLS=14
|
RESULT_BYTES=14
|
||||||
DEADLINE_MS=15
|
RPC_CALLS=15
|
||||||
QUERY=16
|
DEADLINE_MS=16
|
||||||
REFLECTION=17
|
QUERY=17
|
||||||
SPECIALIZE=18
|
REFLECTION=18
|
||||||
ROOT=19
|
SPECIALIZE=19
|
||||||
DOCUMENT=20
|
ROOT=20
|
||||||
FRAGMENTS=21
|
DOCUMENT=21
|
||||||
VIEW=22
|
FRAGMENTS=22
|
||||||
MAX=23
|
VIEW=23
|
||||||
ALLOW=24
|
MAX=24
|
||||||
POLL=25
|
ALLOW=25
|
||||||
QUERYABLE=26
|
POLL=26
|
||||||
RPC=27
|
QUERYABLE=27
|
||||||
QUERY_REASON=28
|
RPC=28
|
||||||
TYPE=29
|
QUERY_REASON=29
|
||||||
OBJECT=30
|
TYPE=30
|
||||||
STORABLE=31
|
OBJECT=31
|
||||||
IMPLEMENTS=32
|
STORABLE=32
|
||||||
REF=33
|
IMPLEMENTS=33
|
||||||
FRAGMENT=34
|
REF=34
|
||||||
IMPORT=35
|
FRAGMENT=35
|
||||||
EXTERNAL=36
|
IMPORT=36
|
||||||
ATOM=37
|
EXTERNAL=37
|
||||||
INTERFACE=38
|
ATOM=38
|
||||||
INTERFACES=39
|
INTERFACE=39
|
||||||
PACKAGE=40
|
INTERFACES=40
|
||||||
VALUE=41
|
PACKAGE=41
|
||||||
RELATION=42
|
VALUE=42
|
||||||
OPERATION=43
|
RELATION=43
|
||||||
FUNCTION=44
|
OPERATION=44
|
||||||
CONSTRUCTOR=45
|
FUNCTION=45
|
||||||
CONSTRUCTS=46
|
CONSTRUCTOR=46
|
||||||
INPUT=47
|
CONSTRUCTS=47
|
||||||
CONFORM=48
|
INPUT=48
|
||||||
AS=49
|
CONFORM=49
|
||||||
BIND=50
|
AS=50
|
||||||
STATIC=51
|
BIND=51
|
||||||
TO=52
|
STATIC=52
|
||||||
PRIVATE=53
|
TO=53
|
||||||
SHARED=54
|
PRIVATE=54
|
||||||
STATE=55
|
SHARED=55
|
||||||
EDGE=56
|
STATE=56
|
||||||
PROJECTION=57
|
EDGE=57
|
||||||
WITH=58
|
PROJECTION=58
|
||||||
USING=59
|
WITH=59
|
||||||
VIA=60
|
USING=60
|
||||||
MATERIALIZE=61
|
VIA=61
|
||||||
IF=62
|
MATERIALIZE=62
|
||||||
ABSENT=63
|
IF=63
|
||||||
ON=64
|
ABSENT=64
|
||||||
POLICY=65
|
ON=65
|
||||||
DEFAULT=66
|
POLICY=66
|
||||||
SOURCE=67
|
DEFAULT=67
|
||||||
REPOSITORY=68
|
SOURCE=68
|
||||||
COMMIT=69
|
REPOSITORY=69
|
||||||
REVISION=70
|
COMMIT=70
|
||||||
SEMANTIC_MAJOR=71
|
REVISION=71
|
||||||
ON_DELETE=72
|
SEMANTIC_MAJOR=72
|
||||||
RETAIN_OTHER=73
|
ON_DELETE=73
|
||||||
KEYED=74
|
RETAIN_OTHER=74
|
||||||
PUBLIC_TRAVERSAL=75
|
KEYED=75
|
||||||
ID=76
|
PUBLIC_TRAVERSAL=76
|
||||||
DOC=77
|
ID=77
|
||||||
MODE=78
|
DOC=78
|
||||||
EMITS=79
|
MODE=79
|
||||||
RECEIVER=80
|
EMITS=80
|
||||||
REQUIRES=81
|
RECEIVER=81
|
||||||
ANY=82
|
REQUIRES=82
|
||||||
GET=83
|
ANY=83
|
||||||
SET=84
|
GET=84
|
||||||
WATCH=85
|
SET=85
|
||||||
START=86
|
WATCH=86
|
||||||
STOP=87
|
START=87
|
||||||
READ=88
|
STOP=88
|
||||||
WRITE=89
|
READ=89
|
||||||
RESOLVE=90
|
WRITE=90
|
||||||
CONNECT=91
|
RESOLVE=91
|
||||||
DISCONNECT=92
|
CONNECT=92
|
||||||
CALL=93
|
DISCONNECT=93
|
||||||
WATCH_START=94
|
CALL=94
|
||||||
WATCH_STOP=95
|
WATCH_START=95
|
||||||
SUBSCRIBE=96
|
WATCH_STOP=96
|
||||||
UNSUBSCRIBE=97
|
SUBSCRIBE=97
|
||||||
REGISTER=98
|
UNSUBSCRIBE=98
|
||||||
CRDT=99
|
REGISTER=99
|
||||||
OPTIONAL_ONE=100
|
CRDT=100
|
||||||
EXACTLY_ONE=101
|
OPTIONAL_ONE=101
|
||||||
MANY_UNIQUE=102
|
EXACTLY_ONE=102
|
||||||
MANY=103
|
MANY_UNIQUE=103
|
||||||
ORDERED=104
|
MANY=104
|
||||||
UNIT=105
|
ORDERED=105
|
||||||
WATCH_HANDLE=106
|
UNIT=106
|
||||||
MESSAGE=107
|
WATCH_HANDLE=107
|
||||||
ATOM_REF=108
|
MESSAGE=108
|
||||||
INTERFACE_REF=109
|
ATOM_REF=109
|
||||||
OPTIONAL=110
|
INTERFACE_REF=110
|
||||||
LIST=111
|
OPTIONAL=111
|
||||||
RECORD=112
|
LIST=112
|
||||||
BOOL=113
|
RECORD=113
|
||||||
BYTES=114
|
BOOL=114
|
||||||
DOUBLE=115
|
BYTES=115
|
||||||
INT32=116
|
DOUBLE=116
|
||||||
INT64=117
|
INT32=117
|
||||||
STRING=118
|
INT64=118
|
||||||
UINT32=119
|
STRING=119
|
||||||
UINT64=120
|
UINT32=120
|
||||||
TRUE=121
|
UINT64=121
|
||||||
FALSE=122
|
TRUE=122
|
||||||
NULL=123
|
FALSE=123
|
||||||
ARROW=124
|
NULL=124
|
||||||
COLON=125
|
ARROW=125
|
||||||
SEMI=126
|
COLON=126
|
||||||
COMMA=127
|
SEMI=127
|
||||||
DOT=128
|
COMMA=128
|
||||||
LBRACE=129
|
DOT=129
|
||||||
RBRACE=130
|
LBRACE=130
|
||||||
LBRACK=131
|
RBRACE=131
|
||||||
RBRACK=132
|
LBRACK=132
|
||||||
LPAREN=133
|
RBRACK=133
|
||||||
RPAREN=134
|
LPAREN=134
|
||||||
LT=135
|
RPAREN=135
|
||||||
GT=136
|
LT=136
|
||||||
AMP=137
|
GT=137
|
||||||
EQUAL=138
|
AMP=138
|
||||||
INTEGER=139
|
EQUAL=139
|
||||||
JSON_NUMBER=140
|
INTEGER=140
|
||||||
IDENTIFIER=141
|
JSON_NUMBER=141
|
||||||
STRING_LITERAL=142
|
IDENTIFIER=142
|
||||||
LINE_COMMENT=143
|
STRING_LITERAL=143
|
||||||
BLOCK_COMMENT=144
|
LINE_COMMENT=144
|
||||||
WS=145
|
BLOCK_COMMENT=145
|
||||||
'creation'=1
|
WS=146
|
||||||
'initialize'=2
|
'edit'=1
|
||||||
'workspace'=3
|
'creation'=2
|
||||||
'execution'=4
|
'initialize'=3
|
||||||
'snapshot'=5
|
'workspace'=4
|
||||||
'transaction'=6
|
'execution'=5
|
||||||
'remote'=7
|
'snapshot'=6
|
||||||
'targets'=8
|
'transaction'=7
|
||||||
'portable'=9
|
'remote'=8
|
||||||
'replica'=10
|
'targets'=9
|
||||||
'local'=11
|
'portable'=10
|
||||||
'from'=12
|
'replica'=11
|
||||||
'result-bytes'=13
|
'local'=12
|
||||||
'rpc-calls'=14
|
'from'=13
|
||||||
'deadline-ms'=15
|
'result-bytes'=14
|
||||||
'query'=16
|
'rpc-calls'=15
|
||||||
'reflection'=17
|
'deadline-ms'=16
|
||||||
'specialize'=18
|
'query'=17
|
||||||
'root'=19
|
'reflection'=18
|
||||||
'document'=20
|
'specialize'=19
|
||||||
'fragments'=21
|
'root'=20
|
||||||
'view'=22
|
'document'=21
|
||||||
'max'=23
|
'fragments'=22
|
||||||
'allow'=24
|
'view'=23
|
||||||
'poll'=25
|
'max'=24
|
||||||
'queryable'=26
|
'allow'=25
|
||||||
'rpc'=27
|
'poll'=26
|
||||||
'query-reason'=28
|
'queryable'=27
|
||||||
'type'=29
|
'rpc'=28
|
||||||
'object'=30
|
'query-reason'=29
|
||||||
'storable'=31
|
'type'=30
|
||||||
'implements'=32
|
'object'=31
|
||||||
'ref'=33
|
'storable'=32
|
||||||
'fragment'=34
|
'implements'=33
|
||||||
'import'=35
|
'ref'=34
|
||||||
'external'=36
|
'fragment'=35
|
||||||
'atom'=37
|
'import'=36
|
||||||
'interface'=38
|
'external'=37
|
||||||
'interfaces'=39
|
'atom'=38
|
||||||
'package'=40
|
'interface'=39
|
||||||
'value'=41
|
'interfaces'=40
|
||||||
'relation'=42
|
'package'=41
|
||||||
'operation'=43
|
'value'=42
|
||||||
'function'=44
|
'relation'=43
|
||||||
'constructor'=45
|
'operation'=44
|
||||||
'constructs'=46
|
'function'=45
|
||||||
'input'=47
|
'constructor'=46
|
||||||
'conform'=48
|
'constructs'=47
|
||||||
'as'=49
|
'input'=48
|
||||||
'bind'=50
|
'conform'=49
|
||||||
'static'=51
|
'as'=50
|
||||||
'to'=52
|
'bind'=51
|
||||||
'private'=53
|
'static'=52
|
||||||
'shared'=54
|
'to'=53
|
||||||
'state'=55
|
'private'=54
|
||||||
'edge'=56
|
'shared'=55
|
||||||
'projection'=57
|
'state'=56
|
||||||
'with'=58
|
'edge'=57
|
||||||
'using'=59
|
'projection'=58
|
||||||
'via'=60
|
'with'=59
|
||||||
'materialize'=61
|
'using'=60
|
||||||
'if'=62
|
'via'=61
|
||||||
'absent'=63
|
'materialize'=62
|
||||||
'on'=64
|
'if'=63
|
||||||
'policy'=65
|
'absent'=64
|
||||||
'default'=66
|
'on'=65
|
||||||
'source'=67
|
'policy'=66
|
||||||
'repository'=68
|
'default'=67
|
||||||
'commit'=69
|
'source'=68
|
||||||
'revision'=70
|
'repository'=69
|
||||||
'semantic-major'=71
|
'commit'=70
|
||||||
'on-delete'=72
|
'revision'=71
|
||||||
'retain-other'=73
|
'semantic-major'=72
|
||||||
'keyed'=74
|
'on-delete'=73
|
||||||
'public-traversal'=75
|
'retain-other'=74
|
||||||
'id'=76
|
'keyed'=75
|
||||||
'doc'=77
|
'public-traversal'=76
|
||||||
'mode'=78
|
'id'=77
|
||||||
'emits'=79
|
'doc'=78
|
||||||
'receiver'=80
|
'mode'=79
|
||||||
'requires'=81
|
'emits'=80
|
||||||
'any'=82
|
'receiver'=81
|
||||||
'get'=83
|
'requires'=82
|
||||||
'set'=84
|
'any'=83
|
||||||
'watch'=85
|
'get'=84
|
||||||
'start'=86
|
'set'=85
|
||||||
'stop'=87
|
'watch'=86
|
||||||
'read'=88
|
'start'=87
|
||||||
'write'=89
|
'stop'=88
|
||||||
'resolve'=90
|
'read'=89
|
||||||
'connect'=91
|
'write'=90
|
||||||
'disconnect'=92
|
'resolve'=91
|
||||||
'call'=93
|
'connect'=92
|
||||||
'watch-start'=94
|
'disconnect'=93
|
||||||
'watch-stop'=95
|
'call'=94
|
||||||
'subscribe'=96
|
'watch-start'=95
|
||||||
'unsubscribe'=97
|
'watch-stop'=96
|
||||||
'register'=98
|
'subscribe'=97
|
||||||
'crdt'=99
|
'unsubscribe'=98
|
||||||
'optional-one'=100
|
'register'=99
|
||||||
'exactly-one'=101
|
'crdt'=100
|
||||||
'many-unique'=102
|
'optional-one'=101
|
||||||
'many'=103
|
'exactly-one'=102
|
||||||
'ordered'=104
|
'many-unique'=103
|
||||||
'unit'=105
|
'many'=104
|
||||||
'watch-handle'=106
|
'ordered'=105
|
||||||
'message'=107
|
'unit'=106
|
||||||
'atom-ref'=108
|
'watch-handle'=107
|
||||||
'interface-ref'=109
|
'message'=108
|
||||||
'optional'=110
|
'atom-ref'=109
|
||||||
'list'=111
|
'interface-ref'=110
|
||||||
'record'=112
|
'optional'=111
|
||||||
'bool'=113
|
'list'=112
|
||||||
'bytes'=114
|
'record'=113
|
||||||
'double'=115
|
'bool'=114
|
||||||
'int32'=116
|
'bytes'=115
|
||||||
'int64'=117
|
'double'=116
|
||||||
'string'=118
|
'int32'=117
|
||||||
'uint32'=119
|
'int64'=118
|
||||||
'uint64'=120
|
'string'=119
|
||||||
'true'=121
|
'uint32'=120
|
||||||
'false'=122
|
'uint64'=121
|
||||||
'null'=123
|
'true'=122
|
||||||
'->'=124
|
'false'=123
|
||||||
':'=125
|
'null'=124
|
||||||
';'=126
|
'->'=125
|
||||||
','=127
|
':'=126
|
||||||
'.'=128
|
';'=127
|
||||||
'{'=129
|
','=128
|
||||||
'}'=130
|
'.'=129
|
||||||
'['=131
|
'{'=130
|
||||||
']'=132
|
'}'=131
|
||||||
'('=133
|
'['=132
|
||||||
')'=134
|
']'=133
|
||||||
'<'=135
|
'('=134
|
||||||
'>'=136
|
')'=135
|
||||||
'&'=137
|
'<'=136
|
||||||
'='=138
|
'>'=137
|
||||||
|
'&'=138
|
||||||
|
'='=139
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -108,6 +108,7 @@ export class GenericSourceTypes {
|
|||||||
return parameter?.kind === "value" && Boolean(parameter.storable);
|
return parameter?.kind === "value" && Boolean(parameter.storable);
|
||||||
}
|
}
|
||||||
if (type.kind === "list" || type.kind === "optional") return storable(type.value, depth + 1);
|
if (type.kind === "list" || type.kind === "optional") return storable(type.value, depth + 1);
|
||||||
|
if (type.kind === "record") return Object.values(type.fields).every((field) => storable(field, depth + 1));
|
||||||
if (type.kind === "alias") {
|
if (type.kind === "alias") {
|
||||||
const alias = aliases.get(type.definitionId);
|
const alias = aliases.get(type.definitionId);
|
||||||
if (!alias || alias.parameters.length !== type.arguments.length) return false;
|
if (!alias || alias.parameters.length !== type.arguments.length) return false;
|
||||||
@@ -318,6 +319,12 @@ export class GenericSourceTypes {
|
|||||||
return { kind: "record", fields: Object.fromEntries(fields) };
|
return { kind: "record", fields: Object.fromEntries(fields) };
|
||||||
}
|
}
|
||||||
const name = context.identifier()!.getText();
|
const name = context.identifier()!.getText();
|
||||||
|
if (name === "change" && context.typeArguments())
|
||||||
|
throw new GenericTypeError(
|
||||||
|
"unsupported-prepared-change",
|
||||||
|
name,
|
||||||
|
"Authored change<T> transport values require LF-19; native edit contracts are available separately",
|
||||||
|
);
|
||||||
if (context.ATOM_REF()) return { kind: "object-ref", expectation: this.atom(name) };
|
if (context.ATOM_REF()) return { kind: "object-ref", expectation: this.atom(name) };
|
||||||
if (context.INTERFACE_REF())
|
if (context.INTERFACE_REF())
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -405,6 +405,8 @@ const valueOperation = (displayName: string, id: InterfaceOperation["id"], value
|
|||||||
outputType: value,
|
outputType: value,
|
||||||
mode: "call",
|
mode: "call",
|
||||||
};
|
};
|
||||||
|
case "edit":
|
||||||
|
return { id, displayName, inputType: { kind: "change", value }, outputType: valueType.unit, mode: "call" };
|
||||||
case "set":
|
case "set":
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
@@ -494,6 +496,11 @@ const lowerValueMember = (state: LoweringState, context: ValueMemberContext): In
|
|||||||
...valueOperation("get", capabilityId.operation(stringValue(operation.stringLiteral(0))), memberValueType),
|
...valueOperation("get", capabilityId.operation(stringValue(operation.stringLiteral(0))), memberValueType),
|
||||||
...lowerExecution(operation.executionClause()),
|
...lowerExecution(operation.executionClause()),
|
||||||
});
|
});
|
||||||
|
} else if (operation.EDIT()) {
|
||||||
|
operations.push({
|
||||||
|
...valueOperation("edit", capabilityId.operation(stringValue(operation.stringLiteral(0))), memberValueType),
|
||||||
|
...lowerExecution(operation.executionClause()),
|
||||||
|
});
|
||||||
} else if (operation.SET()) {
|
} else if (operation.SET()) {
|
||||||
operations.push({
|
operations.push({
|
||||||
...valueOperation("set", capabilityId.operation(stringValue(operation.stringLiteral(0))), memberValueType),
|
...valueOperation("set", capabilityId.operation(stringValue(operation.stringLiteral(0))), memberValueType),
|
||||||
@@ -763,6 +770,13 @@ const lowerInterfaceTemplate = (
|
|||||||
return [
|
return [
|
||||||
{ ...signature("get", id, valueType.unit, type), ...lowerExecution(operation.executionClause()) },
|
{ ...signature("get", id, valueType.unit, type), ...lowerExecution(operation.executionClause()) },
|
||||||
];
|
];
|
||||||
|
if (operation.EDIT())
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
...signature("edit", id, { kind: "change", value: type }, valueType.unit),
|
||||||
|
...lowerExecution(operation.executionClause()),
|
||||||
|
},
|
||||||
|
];
|
||||||
if (operation.SET())
|
if (operation.SET())
|
||||||
return [
|
return [
|
||||||
{ ...signature("set", id, type, valueType.unit), ...lowerExecution(operation.executionClause()) },
|
{ ...signature("set", id, type, valueType.unit), ...lowerExecution(operation.executionClause()) },
|
||||||
@@ -939,7 +953,9 @@ const lowerDependencyPort = (state: LoweringState, context: DependencyPortContex
|
|||||||
.primitiveList()!
|
.primitiveList()!
|
||||||
.primitive()
|
.primitive()
|
||||||
.map((entry) => text(entry));
|
.map((entry) => text(entry));
|
||||||
const invalid = primitives.filter((entry) => !["read", "write", "watch-start", "watch-stop"].includes(entry));
|
const invalid = primitives.filter(
|
||||||
|
(entry) => !["read", "write", "edit", "watch-start", "watch-stop"].includes(entry),
|
||||||
|
);
|
||||||
if (invalid.length > 0) {
|
if (invalid.length > 0) {
|
||||||
loweringIssue(state, context, "invalid-port-primitive", `State port cannot request ${invalid.join(", ")}`);
|
loweringIssue(state, context, "invalid-port-primitive", `State port cannot request ${invalid.join(", ")}`);
|
||||||
}
|
}
|
||||||
@@ -1405,12 +1421,12 @@ const lowerPackage = (
|
|||||||
const name = identifier(clause.identifier(0));
|
const name = identifier(clause.identifier(0));
|
||||||
const key = budgetNames[name],
|
const key = budgetNames[name],
|
||||||
n = Number(clause.INTEGER()!.getText());
|
n = Number(clause.INTEGER()!.getText());
|
||||||
if (!key || assigned.has(name) || !Number.isSafeInteger(n) || n < 1 || n > defaultQueryBudgets[key])
|
if (!key || assigned.has(name) || !Number.isSafeInteger(n) || n < 1 || n > 0xffffffff)
|
||||||
loweringIssue(
|
loweringIssue(
|
||||||
state,
|
state,
|
||||||
clause,
|
clause,
|
||||||
"invalid-query-budget",
|
"invalid-query-budget",
|
||||||
`Invalid, duplicate, or excessive query budget ${name}`,
|
`Invalid, duplicate, or out-of-range query budget ${name}`,
|
||||||
);
|
);
|
||||||
else {
|
else {
|
||||||
declaration.budgets[key] = n;
|
declaration.budgets[key] = n;
|
||||||
@@ -1898,6 +1914,7 @@ const lowerConformance = (
|
|||||||
const primitives = {
|
const primitives = {
|
||||||
get: "read",
|
get: "read",
|
||||||
set: "write",
|
set: "write",
|
||||||
|
edit: "edit",
|
||||||
"watch-start": "watch-start",
|
"watch-start": "watch-start",
|
||||||
"watch-stop": "watch-stop",
|
"watch-stop": "watch-stop",
|
||||||
} as const;
|
} as const;
|
||||||
@@ -2622,7 +2639,7 @@ const compileCapabilityResourceSourceInternal = (
|
|||||||
portNames.add(port.displayName);
|
portNames.add(port.displayName);
|
||||||
const requirement = port.requirement;
|
const requirement = port.requirement;
|
||||||
if (requirement.kind === "state") {
|
if (requirement.kind === "state") {
|
||||||
if (requirement.primitives.some((p) => !["read", "write", "watch-start", "watch-stop"].includes(p)))
|
if (requirement.primitives.some((p) => !["read", "write", "edit", "watch-start", "watch-stop"].includes(p)))
|
||||||
throw new GenericTypeError("invalid-port-primitive", port.displayName, "Invalid state primitive");
|
throw new GenericTypeError("invalid-port-primitive", port.displayName, "Invalid state primitive");
|
||||||
types.push(requirement.valueType);
|
types.push(requirement.valueType);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export type ValueTypeExpression =
|
|||||||
| Extract<ValueType, { kind: "builtin" | "scalar" | "message" }>
|
| Extract<ValueType, { kind: "builtin" | "scalar" | "message" }>
|
||||||
| { kind: "parameter"; parameterId: string }
|
| { kind: "parameter"; parameterId: string }
|
||||||
| { kind: "record"; fields: Record<string, ValueTypeExpression> }
|
| { kind: "record"; fields: Record<string, ValueTypeExpression> }
|
||||||
| { kind: "list" | "optional"; value: ValueTypeExpression }
|
| { kind: "list" | "optional" | "change"; value: ValueTypeExpression }
|
||||||
| { kind: "object-ref"; expectation: ObjectTypeExpression }
|
| { kind: "object-ref"; expectation: ObjectTypeExpression }
|
||||||
| { kind: "alias"; definitionId: string; arguments: TypeArgumentExpression[] };
|
| { kind: "alias"; definitionId: string; arguments: TypeArgumentExpression[] };
|
||||||
|
|
||||||
@@ -184,10 +184,10 @@ export const isStorableType = (
|
|||||||
case "list":
|
case "list":
|
||||||
case "optional":
|
case "optional":
|
||||||
return visit(value.value, depth + 1);
|
return visit(value.value, depth + 1);
|
||||||
// Records currently have RPC codecs, not ordinary-state persistence codecs.
|
case "change":
|
||||||
// A generic bound must not promise storage that the installed model rejects.
|
|
||||||
case "record":
|
|
||||||
return false;
|
return false;
|
||||||
|
case "record":
|
||||||
|
return Object.values(value.fields).every((field) => visit(field, depth + 1));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return visit(type, 0);
|
return visit(type, 0);
|
||||||
@@ -216,6 +216,7 @@ export const canonicalTypeArgument = (argument: ClosedTypeArgument): string => {
|
|||||||
return ["message", value.descriptorId];
|
return ["message", value.descriptorId];
|
||||||
case "object-ref":
|
case "object-ref":
|
||||||
return ["object-ref", target(value.expectation)];
|
return ["object-ref", target(value.expectation)];
|
||||||
|
case "change":
|
||||||
case "list":
|
case "list":
|
||||||
case "optional":
|
case "optional":
|
||||||
return [value.kind, type(value.value, depth + 1)];
|
return [value.kind, type(value.value, depth + 1)];
|
||||||
@@ -323,6 +324,7 @@ export class TypeSubstitution {
|
|||||||
return { kind: "scalar", name: expression.name };
|
return { kind: "scalar", name: expression.name };
|
||||||
case "message":
|
case "message":
|
||||||
return { kind: "message", descriptorId: expression.descriptorId };
|
return { kind: "message", descriptorId: expression.descriptorId };
|
||||||
|
case "change":
|
||||||
case "list":
|
case "list":
|
||||||
case "optional":
|
case "optional":
|
||||||
return { kind: expression.kind, value: this.value(expression.value, `${path}.${expression.kind}`, depth + 1) };
|
return { kind: expression.kind, value: this.value(expression.value, `${path}.${expression.kind}`, depth + 1) };
|
||||||
|
|||||||
@@ -67,7 +67,8 @@ export type ValueType =
|
|||||||
| { kind: "record"; fields: Record<string, ValueType> }
|
| { kind: "record"; fields: Record<string, ValueType> }
|
||||||
| { kind: "object-ref"; expectation: ObjectExpectation }
|
| { kind: "object-ref"; expectation: ObjectExpectation }
|
||||||
| { kind: "optional"; value: ValueType }
|
| { kind: "optional"; value: ValueType }
|
||||||
| { kind: "list"; value: ValueType };
|
| { kind: "list"; value: ValueType }
|
||||||
|
| { kind: "change"; value: ValueType };
|
||||||
|
|
||||||
export const valueType = {
|
export const valueType = {
|
||||||
unit: { kind: "builtin", name: "unit" } as const,
|
unit: { kind: "builtin", name: "unit" } as const,
|
||||||
@@ -214,7 +215,7 @@ export interface EdgeDefinition {
|
|||||||
|
|
||||||
export type PersistentAttachment = StateSlotDefinition | EdgeDefinition;
|
export type PersistentAttachment = StateSlotDefinition | EdgeDefinition;
|
||||||
|
|
||||||
export type StatePrimitive = "read" | "write" | "watch-start" | "watch-stop";
|
export type StatePrimitive = "read" | "write" | "edit" | "watch-start" | "watch-stop";
|
||||||
|
|
||||||
export type EdgePrimitive = "resolve" | "connect" | "disconnect" | "watch-start" | "watch-stop";
|
export type EdgePrimitive = "resolve" | "connect" | "disconnect" | "watch-start" | "watch-stop";
|
||||||
|
|
||||||
|
|||||||
@@ -154,6 +154,8 @@ const typeLabel = (type: ValueType): string => {
|
|||||||
return type.expectation.kind === "atom"
|
return type.expectation.kind === "atom"
|
||||||
? `object:atom:${type.expectation.atomId}`
|
? `object:atom:${type.expectation.atomId}`
|
||||||
: `object:interface:${type.expectation.interfaceRevisionId}`;
|
: `object:interface:${type.expectation.interfaceRevisionId}`;
|
||||||
|
case "change":
|
||||||
|
return `change<${typeLabel(type.value)}>`;
|
||||||
case "optional":
|
case "optional":
|
||||||
return `optional<${typeLabel(type.value)}>`;
|
return `optional<${typeLabel(type.value)}>`;
|
||||||
case "list":
|
case "list":
|
||||||
@@ -189,6 +191,7 @@ export const valueTypesEqual = (left: ValueType, right: ValueType): boolean => {
|
|||||||
? left.expectation.atomId === (other as typeof left.expectation).atomId
|
? left.expectation.atomId === (other as typeof left.expectation).atomId
|
||||||
: left.expectation.interfaceRevisionId === (other as typeof left.expectation).interfaceRevisionId;
|
: left.expectation.interfaceRevisionId === (other as typeof left.expectation).interfaceRevisionId;
|
||||||
}
|
}
|
||||||
|
case "change":
|
||||||
case "optional":
|
case "optional":
|
||||||
case "list":
|
case "list":
|
||||||
return valueTypesEqual(left.value, (right as typeof left).value);
|
return valueTypesEqual(left.value, (right as typeof left).value);
|
||||||
@@ -239,6 +242,7 @@ const validateValueType = (
|
|||||||
case "message":
|
case "message":
|
||||||
requireText(issues, type.descriptorId, `${path}.descriptorId`, "Message descriptor identity");
|
requireText(issues, type.descriptorId, `${path}.descriptorId`, "Message descriptor identity");
|
||||||
return;
|
return;
|
||||||
|
case "change":
|
||||||
case "optional":
|
case "optional":
|
||||||
case "list":
|
case "list":
|
||||||
validateValueType(issues, type.value, `${path}.value`, indexes);
|
validateValueType(issues, type.value, `${path}.value`, indexes);
|
||||||
@@ -341,6 +345,8 @@ export const statePrimitiveSignature = (slot: StateSlotDefinition, primitive: St
|
|||||||
inputType: valueType.unit,
|
inputType: valueType.unit,
|
||||||
outputType: slot.valueType,
|
outputType: slot.valueType,
|
||||||
};
|
};
|
||||||
|
case "edit":
|
||||||
|
return { mode: "call", inputType: { kind: "change", value: slot.valueType }, outputType: valueType.unit };
|
||||||
case "write":
|
case "write":
|
||||||
return {
|
return {
|
||||||
mode: "call",
|
mode: "call",
|
||||||
@@ -532,9 +538,19 @@ const collectIdentityIndexes = (
|
|||||||
if (member.kind === "relationship" && contract === "rpc-permitted")
|
if (member.kind === "relationship" && contract === "rpc-permitted")
|
||||||
issue(issues, "invalid-query-contract", memberPath, "RPC query relationships are not supported");
|
issue(issues, "invalid-query-contract", memberPath, "RPC query relationships are not supported");
|
||||||
if (member.kind === "value") {
|
if (member.kind === "value") {
|
||||||
const type = member.valueType.kind === "optional" ? member.valueType.value : member.valueType;
|
const queryValue = (type: ValueType): boolean =>
|
||||||
if (type.kind !== "scalar")
|
type.kind === "scalar" ||
|
||||||
issue(issues, "invalid-query-contract", memberPath, "Queryable values must be scalar or optional scalar");
|
((type.kind === "optional" || type.kind === "list") && queryValue(type.value)) ||
|
||||||
|
(type.kind === "record" &&
|
||||||
|
Object.keys(type.fields).length > 0 &&
|
||||||
|
Object.values(type.fields).every(queryValue));
|
||||||
|
if (!queryValue(member.valueType))
|
||||||
|
issue(
|
||||||
|
issues,
|
||||||
|
"invalid-query-contract",
|
||||||
|
memberPath,
|
||||||
|
"Queryable values require scalar leaves in optional, list, or nonempty record shapes",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
requireText(issues, member.id, `${memberPath}.id`, "Member ID");
|
requireText(issues, member.id, `${memberPath}.id`, "Member ID");
|
||||||
@@ -837,7 +853,8 @@ const validateAttachments = (issues: CapabilityValidationIssue[], indexes: Valid
|
|||||||
validateValueType(issues, attachment.valueType, `${path}.valueType`, indexes);
|
validateValueType(issues, attachment.valueType, `${path}.valueType`, indexes);
|
||||||
const containsRpcType = (type: ValueType): boolean =>
|
const containsRpcType = (type: ValueType): boolean =>
|
||||||
type.kind === "object-ref" ||
|
type.kind === "object-ref" ||
|
||||||
type.kind === "record" ||
|
type.kind === "change" ||
|
||||||
|
(type.kind === "record" && Object.values(type.fields).some(containsRpcType)) ||
|
||||||
((type.kind === "optional" || type.kind === "list") && containsRpcType(type.value));
|
((type.kind === "optional" || type.kind === "list") && containsRpcType(type.value));
|
||||||
if (
|
if (
|
||||||
containsRpcType(attachment.valueType) ||
|
containsRpcType(attachment.valueType) ||
|
||||||
@@ -847,7 +864,7 @@ const validateAttachments = (issues: CapabilityValidationIssue[], indexes: Valid
|
|||||||
issues,
|
issues,
|
||||||
"invalid-attachment",
|
"invalid-attachment",
|
||||||
`${path}.valueType`,
|
`${path}.valueType`,
|
||||||
"Managed object references belong in graph relationships, not ordinary state; record types are RPC-only",
|
"Managed object references belong in graph relationships; captured changes are transport-only",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (attachment.storagePolicy.kind === "crdt-document") {
|
if (attachment.storagePolicy.kind === "crdt-document") {
|
||||||
@@ -1287,6 +1304,12 @@ const validateBoundDependencies = (
|
|||||||
: `State ${stateBinding.slotId} is not attached to receiver atom ${params.atomId}`,
|
: `State ${stateBinding.slotId} is not attached to receiver atom ${params.atomId}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
requirement.primitives.includes("edit") &&
|
||||||
|
(attachment.attachment.storagePolicy.kind !== "crdt-document" ||
|
||||||
|
!valueTypesEqual(attachment.attachment.valueType, attachment.attachment.storagePolicy.updateType))
|
||||||
|
)
|
||||||
|
issue(issues, "invalid-dependency-binding", path, "Edit ports require a matching CRDT slot");
|
||||||
if (!valueTypesEqual(requirement.valueType, attachment.attachment.valueType)) {
|
if (!valueTypesEqual(requirement.valueType, attachment.attachment.valueType)) {
|
||||||
issue(
|
issue(
|
||||||
issues,
|
issues,
|
||||||
@@ -1579,6 +1602,17 @@ const validateConformances = (
|
|||||||
}
|
}
|
||||||
} else binding = entry.binding;
|
} else binding = entry.binding;
|
||||||
const member = operationEntry.member;
|
const member = operationEntry.member;
|
||||||
|
if (
|
||||||
|
member.kind === "value" &&
|
||||||
|
operation.displayName === "edit" &&
|
||||||
|
(binding.kind !== "state" || binding.primitive !== "edit")
|
||||||
|
)
|
||||||
|
issue(
|
||||||
|
issues,
|
||||||
|
"invalid-state-binding",
|
||||||
|
bindingPath,
|
||||||
|
"CRDT edit operations must resolve to a native state edit capability",
|
||||||
|
);
|
||||||
if (
|
if (
|
||||||
member.kind !== "operation" &&
|
member.kind !== "operation" &&
|
||||||
member.queryRead &&
|
member.queryRead &&
|
||||||
@@ -1652,6 +1686,17 @@ const validateConformances = (
|
|||||||
"State primitives may only implement value members",
|
"State primitives may only implement value members",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
binding.primitive === "edit" &&
|
||||||
|
(attachment.attachment.storagePolicy.kind !== "crdt-document" ||
|
||||||
|
!valueTypesEqual(attachment.attachment.valueType, attachment.attachment.storagePolicy.updateType))
|
||||||
|
)
|
||||||
|
issue(
|
||||||
|
issues,
|
||||||
|
"invalid-state-binding",
|
||||||
|
bindingPath,
|
||||||
|
"Edit requires a CRDT slot with a matching materialized profile type",
|
||||||
|
);
|
||||||
const expected = statePrimitiveSignature(attachment.attachment, binding.primitive);
|
const expected = statePrimitiveSignature(attachment.attachment, binding.primitive);
|
||||||
if (!signaturesMatch(operation, expected)) {
|
if (!signaturesMatch(operation, expected)) {
|
||||||
issue(
|
issue(
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -154,8 +154,19 @@ export function relationalCompiler(
|
|||||||
): QueryExpression => {
|
): QueryExpression => {
|
||||||
requireAvailable(names, available, node);
|
requireAvailable(names, available, node);
|
||||||
const meta = names.at(-2) === "_qx";
|
const meta = names.at(-2) === "_qx";
|
||||||
const { row, path, nullable } = walk(root, names.slice(0, meta ? -2 : -1), uses, node);
|
let fieldIndex = names.length - 1;
|
||||||
const name = names.at(-1)!;
|
if (!meta) {
|
||||||
|
let cursor = root;
|
||||||
|
for (let index = 0; index < names.length; index++) {
|
||||||
|
if (cursor.kind === "object" && rowFields(cursor).get(names[index]!)?.kind === "leaf") {
|
||||||
|
fieldIndex = index;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (index < names.length - 1) cursor = walk(root, names.slice(0, index + 1), uses, node).row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const { row, path, nullable } = walk(root, names.slice(0, meta ? -2 : fieldIndex), uses, node);
|
||||||
|
const name = names[meta ? names.length - 1 : fieldIndex]!;
|
||||||
let type: ValueType, leaf: QueryExpression["leaf"];
|
let type: ValueType, leaf: QueryExpression["leaf"];
|
||||||
if (meta) {
|
if (meta) {
|
||||||
if (name === "ref" && row.kind === "object") {
|
if (name === "ref" && row.kind === "object") {
|
||||||
@@ -185,7 +196,24 @@ export function relationalCompiler(
|
|||||||
for (const use of uses) mark(row.interfaceRevisionId, name, use, node);
|
for (const use of uses) mark(row.interfaceRevisionId, name, use, node);
|
||||||
const member = env.contracts.get(row.interfaceRevisionId)!.members.find((m) => m.displayName === name)!;
|
const member = env.contracts.get(row.interfaceRevisionId)!.members.find((m) => m.displayName === name)!;
|
||||||
type = field.type;
|
type = field.type;
|
||||||
leaf = { kind: "field", interfaceRevisionId: row.interfaceRevisionId, memberId: member.id };
|
const valuePath = names.slice(fieldIndex + 1);
|
||||||
|
let valueNullable = false;
|
||||||
|
for (const component of valuePath) {
|
||||||
|
if (type.kind === "optional") {
|
||||||
|
valueNullable = true;
|
||||||
|
type = type.value;
|
||||||
|
}
|
||||||
|
if (type.kind !== "record" || !Object.hasOwn(type.fields, component))
|
||||||
|
return fail("QUERY_FIELD_NOT_QUERYABLE", `Unknown scalar value path ${names.join(".")}`, node);
|
||||||
|
type = type.fields[component]!;
|
||||||
|
}
|
||||||
|
if (valueNullable && type.kind !== "optional") type = valueType.optional(type);
|
||||||
|
leaf = {
|
||||||
|
kind: "field",
|
||||||
|
interfaceRevisionId: row.interfaceRevisionId,
|
||||||
|
memberId: member.id,
|
||||||
|
...(valuePath.length ? { valuePath } : {}),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return { path, leaf, type: nullable && type.kind !== "optional" ? valueType.optional(type) : type };
|
return { path, leaf, type: nullable && type.kind !== "optional" ? valueType.optional(type) : type };
|
||||||
};
|
};
|
||||||
@@ -356,15 +384,22 @@ export function relationalCompiler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const expr = expression(row, [...base, name], ["predicate"], field, available);
|
const valuePredicate = (names: string[], value: ValueNode) => {
|
||||||
children.push(
|
const expr = expression(row, names, ["predicate"], field, available);
|
||||||
...(comparisons(field.value, (operator, value) => ({
|
const type = expr.type.kind === "optional" ? expr.type.value : expr.type;
|
||||||
kind: "compare",
|
if (type.kind === "record") {
|
||||||
expression: expr,
|
for (const child of pairs(value)) valuePredicate([...names, child.name.value], child.value);
|
||||||
operator,
|
} else
|
||||||
value,
|
children.push(
|
||||||
})) as QueryPredicate[]),
|
...(comparisons(value, (operator, value) => ({
|
||||||
);
|
kind: "compare",
|
||||||
|
expression: expr,
|
||||||
|
operator,
|
||||||
|
value,
|
||||||
|
})) as QueryPredicate[]),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
valuePredicate([...base, name], field.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { comparableValue } from "./structured.js";
|
||||||
import {
|
import {
|
||||||
GraphQLBoolean,
|
GraphQLBoolean,
|
||||||
GraphQLString,
|
GraphQLString,
|
||||||
@@ -45,6 +46,7 @@ export interface RelationalSchemaEnvironment {
|
|||||||
name(id: InterfaceRevisionId): string;
|
name(id: InterfaceRevisionId): string;
|
||||||
scalar(type: ValueType): GraphQLInputType & GraphQLOutputType;
|
scalar(type: ValueType): GraphQLInputType & GraphQLOutputType;
|
||||||
comparison(type: ValueType): GraphQLInputObjectType;
|
comparison(type: ValueType): GraphQLInputObjectType;
|
||||||
|
valueOrder(type: ValueType): GraphQLInputType;
|
||||||
scalarTypes: Map<string, ValueType>;
|
scalarTypes: Map<string, ValueType>;
|
||||||
direction: GraphQLEnumType;
|
direction: GraphQLEnumType;
|
||||||
cursor: GraphQLScalarType;
|
cursor: GraphQLScalarType;
|
||||||
@@ -255,7 +257,8 @@ export function relationalSchema(env: RelationalSchemaEnvironment, document?: Do
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result[name] = { type: ordering ? env.direction : compare(type) };
|
if (!comparableValue(type, ordering)) continue;
|
||||||
|
result[name] = { type: ordering ? env.valueOrder(type) : compare(type) };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const meta: GraphQLInputFieldConfigMap = {};
|
const meta: GraphQLInputFieldConfigMap = {};
|
||||||
@@ -300,6 +303,7 @@ export function relationalSchema(env: RelationalSchemaEnvironment, document?: Do
|
|||||||
for (const [name, field] of fields(row)) {
|
for (const [name, field] of fields(row)) {
|
||||||
if (row.kind === "object" && field.kind === "row") continue;
|
if (row.kind === "object" && field.kind === "row") continue;
|
||||||
if (name in result) throw new QueryCompileError("QUERY_SCHEMA_NAME", `Reserved predicate name ${name}`);
|
if (name in result) throw new QueryCompileError("QUERY_SCHEMA_NAME", `Reserved predicate name ${name}`);
|
||||||
|
if (field.kind === "leaf" && !comparableValue(field.type)) continue;
|
||||||
result[name] = { type: field.kind === "row" ? where(field.row) : compare(field.type) };
|
result[name] = { type: field.kind === "row" ? where(field.row) : compare(field.type) };
|
||||||
}
|
}
|
||||||
result._qx = {
|
result._qx = {
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export type QueryPathStep =
|
|||||||
export interface QueryExpression {
|
export interface QueryExpression {
|
||||||
path: QueryPathStep[];
|
path: QueryPathStep[];
|
||||||
leaf:
|
leaf:
|
||||||
| { kind: "field"; interfaceRevisionId: InterfaceRevisionId; memberId: MemberId }
|
| { kind: "field"; interfaceRevisionId: InterfaceRevisionId; memberId: MemberId; valuePath?: string[] }
|
||||||
| { kind: "ref"; interfaceRevisionId: InterfaceRevisionId }
|
| { kind: "ref"; interfaceRevisionId: InterfaceRevisionId }
|
||||||
| { kind: "entry" | "mapKey" };
|
| { kind: "entry" | "mapKey" };
|
||||||
type: ValueType;
|
type: ValueType;
|
||||||
|
|||||||
+18
-7
@@ -1,3 +1,4 @@
|
|||||||
|
import { structuredQueryTypes, comparableValue } from "./structured.js";
|
||||||
import {
|
import {
|
||||||
GraphQLBoolean,
|
GraphQLBoolean,
|
||||||
GraphQLString,
|
GraphQLString,
|
||||||
@@ -132,7 +133,7 @@ export function querySchema(
|
|||||||
throw new QueryCompileError("QUERY_TYPE", "Only scalar/optional scalar query fields are supported");
|
throw new QueryCompileError("QUERY_TYPE", "Only scalar/optional scalar query fields are supported");
|
||||||
return queryScalars[type.name];
|
return queryScalars[type.name];
|
||||||
};
|
};
|
||||||
const comparison = (type: ValueType) => {
|
const scalarComparison = (type: ValueType) => {
|
||||||
const base = type.kind === "optional" ? type.value : type;
|
const base = type.kind === "optional" ? type.value : type;
|
||||||
const value = scalar(base);
|
const value = scalar(base);
|
||||||
const key = String(value);
|
const key = String(value);
|
||||||
@@ -149,8 +150,21 @@ export function querySchema(
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
const structured = structuredQueryTypes(scalar, scalarComparison, direction);
|
||||||
|
const comparison = (type: ValueType) => structured.input(type) as GraphQLInputObjectType;
|
||||||
const relational = relationalSchema(
|
const relational = relationalSchema(
|
||||||
{ contracts, target, name, scalar, comparison, scalarTypes, direction, cursor: cursorScalar, pageInfo },
|
{
|
||||||
|
contracts,
|
||||||
|
target,
|
||||||
|
name,
|
||||||
|
scalar,
|
||||||
|
comparison,
|
||||||
|
valueOrder: (type) => structured.input(type, true),
|
||||||
|
scalarTypes,
|
||||||
|
direction,
|
||||||
|
cursor: cursorScalar,
|
||||||
|
pageInfo,
|
||||||
|
},
|
||||||
document,
|
document,
|
||||||
);
|
);
|
||||||
relational.discover(root.interface);
|
relational.discover(root.interface);
|
||||||
@@ -162,7 +176,7 @@ export function querySchema(
|
|||||||
for (const member of contract(id).members) {
|
for (const member of contract(id).members) {
|
||||||
if (member.kind !== "value" || !member.queryRead) continue;
|
if (member.kind !== "value" || !member.queryRead) continue;
|
||||||
const t = member.valueType.kind === "optional" ? member.valueType.value : member.valueType;
|
const t = member.valueType.kind === "optional" ? member.valueType.value : member.valueType;
|
||||||
if (t.kind === "scalar" && t.name !== "bytes") fields[member.displayName] = { type: direction };
|
if (comparableValue(t, true)) fields[member.displayName] = { type: structured.input(t, true) };
|
||||||
}
|
}
|
||||||
if (Object.keys(fields).length === 0) return undefined;
|
if (Object.keys(fields).length === 0) return undefined;
|
||||||
result = new GraphQLInputObjectType({ name: `${name(id)}Order`, fields });
|
result = new GraphQLInputObjectType({ name: `${name(id)}Order`, fields });
|
||||||
@@ -190,10 +204,7 @@ export function querySchema(
|
|||||||
if (!/^[_A-Za-z][_0-9A-Za-z]*$/.test(member.displayName) || member.displayName.startsWith("_"))
|
if (!/^[_A-Za-z][_0-9A-Za-z]*$/.test(member.displayName) || member.displayName.startsWith("_"))
|
||||||
throw new QueryCompileError("QUERY_SCHEMA_NAME", `Unsupported query field name ${member.displayName}`);
|
throw new QueryCompileError("QUERY_SCHEMA_NAME", `Unsupported query field name ${member.displayName}`);
|
||||||
if (member.kind === "value") {
|
if (member.kind === "value") {
|
||||||
const type = scalar(member.valueType);
|
fields[member.displayName] = { type: structured.output(member.valueType) };
|
||||||
fields[member.displayName] = {
|
|
||||||
type: member.valueType.kind === "optional" ? type : new GraphQLNonNull(type),
|
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
const targetId = target(member),
|
const targetId = target(member),
|
||||||
node = object(targetId);
|
node = object(targetId);
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { createHash } from "node:crypto";
|
||||||
|
import {
|
||||||
|
GraphQLObjectType,
|
||||||
|
GraphQLInputObjectType,
|
||||||
|
GraphQLList,
|
||||||
|
GraphQLNonNull,
|
||||||
|
type GraphQLNullableType,
|
||||||
|
type GraphQLOutputType,
|
||||||
|
type GraphQLInputType,
|
||||||
|
type GraphQLEnumType,
|
||||||
|
} from "graphql";
|
||||||
|
import type { ValueType } from "../capability-model/types.js";
|
||||||
|
import { QueryCompileError } from "./types.js";
|
||||||
|
|
||||||
|
export function comparableValue(type: ValueType, ordering = false): boolean {
|
||||||
|
if (type.kind === "optional") return comparableValue(type.value, ordering);
|
||||||
|
if (type.kind === "record") return Object.values(type.fields).some((t) => comparableValue(t, ordering));
|
||||||
|
return type.kind === "scalar" && (!ordering || type.name !== "bytes");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Structured values retain one field identity; their children are value projections. */
|
||||||
|
export function structuredQueryTypes(
|
||||||
|
scalar: (type: ValueType) => GraphQLInputType & GraphQLOutputType,
|
||||||
|
scalarComparison: (type: ValueType) => GraphQLInputObjectType,
|
||||||
|
direction: GraphQLEnumType,
|
||||||
|
) {
|
||||||
|
const objects = new Map<string, GraphQLObjectType>();
|
||||||
|
const inputs = new Map<string, GraphQLInputObjectType>();
|
||||||
|
const name = (role: string, type: ValueType) =>
|
||||||
|
`QxValue${role}_${createHash("sha256").update(JSON.stringify(type)).digest("hex").slice(0, 16)}`;
|
||||||
|
const output = (type: ValueType, depth = 0): GraphQLOutputType => {
|
||||||
|
if (depth > 128) throw new QueryCompileError("QUERY_TYPE", "Structured value exceeds type depth limit");
|
||||||
|
if (type.kind === "optional") return required(type.value, depth + 1);
|
||||||
|
return new GraphQLNonNull(required(type, depth));
|
||||||
|
};
|
||||||
|
const required = (type: ValueType, depth: number): GraphQLNullableType & GraphQLOutputType => {
|
||||||
|
if (type.kind === "optional") return required(type.value, depth + 1);
|
||||||
|
if (type.kind === "list") return new GraphQLList(output(type.value, depth + 1));
|
||||||
|
if (type.kind !== "record") return scalar(type) as GraphQLNullableType & GraphQLOutputType;
|
||||||
|
const key = name("Record", type);
|
||||||
|
let result = objects.get(key);
|
||||||
|
if (!result) {
|
||||||
|
if (!Object.keys(type.fields).length)
|
||||||
|
throw new QueryCompileError("QUERY_TYPE", "Queryable records must have at least one field");
|
||||||
|
result = new GraphQLObjectType({
|
||||||
|
name: key,
|
||||||
|
fields: () =>
|
||||||
|
Object.fromEntries(
|
||||||
|
Object.entries(type.fields).map(([key, value]) => [key, { type: output(value, depth + 1) }]),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
objects.set(key, result);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
const input = (type: ValueType, ordering = false): GraphQLInputType => {
|
||||||
|
if (type.kind === "optional") return input(type.value, ordering);
|
||||||
|
if (type.kind !== "record") return ordering ? direction : scalarComparison(type);
|
||||||
|
const key = name(ordering ? "Order" : "Compare", type);
|
||||||
|
let result = inputs.get(key);
|
||||||
|
if (!result) {
|
||||||
|
result = new GraphQLInputObjectType({
|
||||||
|
name: key,
|
||||||
|
fields: () =>
|
||||||
|
Object.fromEntries(
|
||||||
|
Object.entries(type.fields)
|
||||||
|
.filter(([, value]) => comparableValue(value, ordering))
|
||||||
|
.map(([key, value]) => [key, { type: input(value, ordering) }]),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
inputs.set(key, result);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
return { output, input };
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { test } from "node:test";
|
||||||
|
import { compileCapabilityResourceSource, compileCapabilitySource } from "../src/capability-language/index.js";
|
||||||
|
const source = { repository: "https://example.test/edit.git", commit: "a".repeat(40) };
|
||||||
|
const content = "record { text:string; tags:list<string>; }";
|
||||||
|
const iface = compileCapabilityResourceSource(
|
||||||
|
`interface Comment id "comment" revision "comment@1" {
|
||||||
|
value content id "content" : ${content} {
|
||||||
|
get id "content:get" execution snapshot targets [browser, server];
|
||||||
|
edit id "content:edit" execution transaction targets [browser, server];
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
{ source },
|
||||||
|
);
|
||||||
|
assert.ok(iface.ok && iface.resource.kind === "interface", JSON.stringify(iface.diagnostics));
|
||||||
|
const contract = iface.resource.revision;
|
||||||
|
const compile = (policy = `crdt(${content})`, binding = "edit") =>
|
||||||
|
compileCapabilitySource(
|
||||||
|
`
|
||||||
|
workspace Comments id "w" revision "w@1" commit "${"a".repeat(40)}" {
|
||||||
|
import interface Comment;
|
||||||
|
atom Comment id "comment";
|
||||||
|
conform Comment as Comment id "owner:comment" {
|
||||||
|
private state Content id "content" on Comment : ${content} policy ${policy};
|
||||||
|
initialize { content to state Content; };
|
||||||
|
bind content.get to state Content.read;
|
||||||
|
bind content.edit to state Content.${binding};
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
"comments.qx",
|
||||||
|
{ interfaces: new Map([["Comment", contract]]), interfaceClosure: [contract] },
|
||||||
|
);
|
||||||
|
test("native edit is an explicit typed change capability over structured content", () => {
|
||||||
|
const result = compile();
|
||||||
|
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
||||||
|
const member = contract.members[0]!;
|
||||||
|
assert.equal(member.kind, "value");
|
||||||
|
if (member.kind !== "value") return;
|
||||||
|
assert.deepEqual(member.operations[1]!.inputType, { kind: "change", value: member.valueType });
|
||||||
|
assert.equal(result.workspace.conformances[0]!.operationBindings[1]!.binding.kind, "state");
|
||||||
|
});
|
||||||
|
test("edit cannot target a register, mismatched profile, or ordinary replacement", () => {
|
||||||
|
for (const [policy, binding] of [
|
||||||
|
["register", "edit"],
|
||||||
|
["crdt(bytes)", "edit"],
|
||||||
|
[`crdt(${content})`, "write"],
|
||||||
|
]) {
|
||||||
|
const result = compile(policy, binding);
|
||||||
|
assert.equal(result.ok, false);
|
||||||
|
assert.ok(
|
||||||
|
result.diagnostics.some((d) => d.code === "invalid-state-binding"),
|
||||||
|
JSON.stringify(result.diagnostics),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
test("authored prepared change transport values fail explicitly until their runtime is implemented", () => {
|
||||||
|
const result = compileCapabilityResourceSource(
|
||||||
|
'package P id "p" revision "p@1" { function applyChange id "apply" : change<string> -> unit; }',
|
||||||
|
{ source },
|
||||||
|
);
|
||||||
|
assert.equal(result.ok, false);
|
||||||
|
assert.ok(
|
||||||
|
result.diagnostics.some((d) => d.code === "unsupported-prepared-change"),
|
||||||
|
JSON.stringify(result.diagnostics),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("generic edit retains its materialized type through specialization", () => {
|
||||||
|
const generic = compileCapabilityResourceSource(
|
||||||
|
`interface Cell<value T : storable> id "cell" revision "cell@1" {
|
||||||
|
value content id "content" : T { get id "get"; edit id "edit" execution transaction targets [browser, server]; }
|
||||||
|
}`,
|
||||||
|
{ source },
|
||||||
|
);
|
||||||
|
assert.ok(generic.ok && generic.resource.kind === "interface", JSON.stringify(generic.diagnostics));
|
||||||
|
const result = compileCapabilitySource(
|
||||||
|
`workspace W id "w" revision "w@1" commit "${"a".repeat(40)}" {
|
||||||
|
import interface Cell;
|
||||||
|
atom Note id "note";
|
||||||
|
conform Note as Cell<${content}> {
|
||||||
|
private state Content id "content" on Note : ${content} policy crdt(${content});
|
||||||
|
bind content to state Content;
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
"cell.qx",
|
||||||
|
{ interfaces: new Map([["Cell", generic.resource.revision]]), interfaceClosure: [generic.resource.revision] },
|
||||||
|
);
|
||||||
|
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
||||||
|
const applied = result.workspace.interfaceImports.find((i) => i.application)!;
|
||||||
|
assert.equal(applied.members[0]!.operations[1]!.inputType.kind, "change");
|
||||||
|
});
|
||||||
@@ -455,8 +455,8 @@ test("expanding recursive generic interfaces fail with a bounded diagnostic", ()
|
|||||||
assert.equal(result.diagnostics[0].code, "recursive-application");
|
assert.equal(result.diagnostics[0].code, "recursive-application");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("storable parameters do not imply support for RPC-only records", () => {
|
test("storable records recursively require storable leaves", () => {
|
||||||
assert.equal(isStorableType({ kind: "record", fields: { name: valueType.string } }), false);
|
assert.equal(isStorableType({ kind: "record", fields: { name: valueType.string } }), true);
|
||||||
assert.equal(isStorableType(valueType.list(valueType.optional(valueType.string))), true);
|
assert.equal(isStorableType(valueType.list(valueType.optional(valueType.string))), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ test("queryable fields need one authoritative getter and supported values", () =
|
|||||||
assert.equal(compile('queryable value title id "title" : string { get id "title:get"; }').ok, true);
|
assert.equal(compile('queryable value title id "title" : string { get id "title:get"; }').ok, true);
|
||||||
for (const member of [
|
for (const member of [
|
||||||
'queryable value title id "title" : string { set id "title:set"; }',
|
'queryable value title id "title" : string { set id "title:set"; }',
|
||||||
'queryable value title id "title" : list<string> { get id "title:get"; }',
|
'queryable value title id "title" : list<unit> { get id "title:get"; }',
|
||||||
'queryable value title id "title" : string { get id "title:get"; get id "other:get"; }',
|
'queryable value title id "title" : string { get id "title:get"; get id "other:get"; }',
|
||||||
])
|
])
|
||||||
assert.equal(compile(member).ok, false, member);
|
assert.equal(compile(member).ok, false, member);
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { test } from "node:test";
|
||||||
|
import { compileCapabilityResourceSource } from "../src/capability-language/index.js";
|
||||||
|
import { compileQuery } from "../src/query/compile.js";
|
||||||
|
import { querySelectionToWire } from "../src/query/proto.js";
|
||||||
|
import { defaultQueryBudgets, type QueryDeclaration } from "../src/query/types.js";
|
||||||
|
const source = { repository: "https://example.test/structured.git", commit: "a".repeat(40) };
|
||||||
|
const result = compileCapabilityResourceSource(
|
||||||
|
`interface Highlight id "highlight" revision "highlight@1" {
|
||||||
|
queryable value range id "range" : record { sourceRevision:string; start:int32; end:int32; } { get id "range:get" execution snapshot targets [browser, server]; }
|
||||||
|
queryable value content id "content" : record { text:string; tags:list<string>; metadata:optional<record { score:int32; }>; } { get id "content:get" execution snapshot targets [browser, server]; }
|
||||||
|
}`,
|
||||||
|
{ source },
|
||||||
|
);
|
||||||
|
assert.ok(result.ok && result.resource.kind === "interface", JSON.stringify(result.diagnostics));
|
||||||
|
const iface = result.resource.revision;
|
||||||
|
const declaration: QueryDeclaration = {
|
||||||
|
id: "rows",
|
||||||
|
displayName: "Rows",
|
||||||
|
root: { kind: "workspace-interface", interface: iface.revisionId },
|
||||||
|
document: "rows.graphql",
|
||||||
|
operation: "Rows",
|
||||||
|
fragments: [],
|
||||||
|
views: [],
|
||||||
|
allowances: [],
|
||||||
|
budgets: { ...defaultQueryBudgets },
|
||||||
|
watch: false,
|
||||||
|
};
|
||||||
|
const compile = (selection: string) =>
|
||||||
|
compileQuery(
|
||||||
|
declaration,
|
||||||
|
[iface],
|
||||||
|
async () =>
|
||||||
|
`query Rows {root {objects(first:10, ${selection}) {entries {node {position:range {begin:start end} content {text tags metadata {score}}}}}}}`,
|
||||||
|
);
|
||||||
|
test("record/list query values preserve nested projections and leaf predicates", async () => {
|
||||||
|
const checked = await compile(
|
||||||
|
"where:{range:{start:{gte:4}},content:{metadata:{score:{gt:2}}}}, orderBy:{range:{start:ASC}}",
|
||||||
|
);
|
||||||
|
const extent = checked.selection[0]!.selection[0]!;
|
||||||
|
assert.equal(extent.predicate?.kind, "and");
|
||||||
|
if (extent.predicate?.kind !== "and") return;
|
||||||
|
const expressions = extent.predicate.children.map((p) => (p.kind === "compare" ? p.expression : undefined));
|
||||||
|
assert.deepEqual(
|
||||||
|
expressions.map((e) => e?.leaf),
|
||||||
|
[
|
||||||
|
{ kind: "field", interfaceRevisionId: "highlight@1", memberId: "range", valuePath: ["start"] },
|
||||||
|
{ kind: "field", interfaceRevisionId: "highlight@1", memberId: "content", valuePath: ["metadata", "score"] },
|
||||||
|
],
|
||||||
|
);
|
||||||
|
assert.deepEqual(expressions[1]?.type, { kind: "optional", value: { kind: "scalar", name: "int32" } });
|
||||||
|
const wire = querySelectionToWire(extent);
|
||||||
|
assert.equal(wire.predicate?.predicate.case, "and");
|
||||||
|
if (wire.predicate?.predicate.case === "and") {
|
||||||
|
const child = wire.predicate.predicate.value.children[0]!;
|
||||||
|
assert.equal(child.predicate.case, "compare");
|
||||||
|
if (child.predicate.case === "compare") {
|
||||||
|
const operand = child.predicate.value.operand?.operand;
|
||||||
|
assert.ok(operand?.case === "expression" && operand.value.leaf.case === "field");
|
||||||
|
assert.deepEqual(operand.value.leaf.value.valuePath, ["start"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const raw = JSON.stringify(checked.output);
|
||||||
|
assert.match(raw, /"begin"/);
|
||||||
|
assert.match(raw, /"tags":\{"kind":"list"/);
|
||||||
|
assert.equal(checked.effects.filter((e) => e.memberId === "range").length, 1);
|
||||||
|
});
|
||||||
|
test("structured predicates reject whole-record scalar comparisons and implicit list expansion", async () => {
|
||||||
|
await assert.rejects(compile("where:{range:{eq:4}}"), /not defined/);
|
||||||
|
await assert.rejects(compile('where:{content:{tags:{eq:"x"}}}'), /not defined/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("explicit budgets can exceed defaults without exceeding wire bounds", () => {
|
||||||
|
const compile = (rows: string) =>
|
||||||
|
compileCapabilityResourceSource(
|
||||||
|
`import interface Highlight;
|
||||||
|
package P id "p" revision "p@1" {
|
||||||
|
query Rows id "rows" root Highlight document "rows.graphql" operation "Rows" { replica; max rows ${rows}; max result-bytes 8388608; }
|
||||||
|
}`,
|
||||||
|
{ source, environment: { interfaces: new Map([["Highlight", iface]]), interfaceClosure: [iface] } },
|
||||||
|
);
|
||||||
|
const result = compile("5000");
|
||||||
|
assert.ok(result.ok && result.resource.kind === "package", JSON.stringify(result.diagnostics));
|
||||||
|
assert.equal(result.resource.revision.queries![0]!.budgets.rows, 5000);
|
||||||
|
assert.equal(compile("4294967296").ok, false);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user