@ultimabase/graphql 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +4 -2
- package/dist/data-types.d.ts +15 -0
- package/dist/data-types.d.ts.map +1 -0
- package/dist/data-types.js +53 -0
- package/dist/data.d.ts +34 -0
- package/dist/data.d.ts.map +1 -0
- package/dist/data.js +207 -0
- package/dist/depth.d.ts +6 -0
- package/dist/depth.d.ts.map +1 -0
- package/dist/depth.js +67 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +42 -0
- package/dist/functions.d.ts +28 -0
- package/dist/functions.d.ts.map +1 -0
- package/dist/functions.js +195 -0
- package/dist/graphiql.d.ts +3 -0
- package/dist/graphiql.d.ts.map +1 -0
- package/dist/graphiql.js +46 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +165 -0
- package/dist/inputs.d.ts +20 -0
- package/dist/inputs.d.ts.map +1 -0
- package/dist/inputs.js +236 -0
- package/dist/json-fields.d.ts +12 -0
- package/dist/json-fields.d.ts.map +1 -0
- package/dist/json-fields.js +66 -0
- package/dist/json-schema.d.ts +7 -0
- package/dist/json-schema.d.ts.map +1 -0
- package/dist/json-schema.js +16 -0
- package/dist/live-stream.d.ts +59 -0
- package/dist/live-stream.d.ts.map +1 -0
- package/dist/live-stream.js +136 -0
- package/dist/mutations.d.ts +9 -0
- package/dist/mutations.d.ts.map +1 -0
- package/dist/mutations.js +228 -0
- package/dist/names.d.ts +8 -0
- package/dist/names.d.ts.map +1 -0
- package/dist/names.js +14 -0
- package/dist/outputs.d.ts +16 -0
- package/dist/outputs.d.ts.map +1 -0
- package/dist/outputs.js +177 -0
- package/dist/overlap.d.ts +5 -0
- package/dist/overlap.d.ts.map +1 -0
- package/dist/overlap.js +82 -0
- package/dist/pages.d.ts +11 -0
- package/dist/pages.d.ts.map +1 -0
- package/dist/pages.js +22 -0
- package/dist/registry.d.ts +80 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +97 -0
- package/dist/reread.d.ts +9 -0
- package/dist/reread.d.ts.map +1 -0
- package/dist/reread.js +116 -0
- package/dist/rows.d.ts +23 -0
- package/dist/rows.d.ts.map +1 -0
- package/dist/rows.js +106 -0
- package/dist/scalars.d.ts +13 -0
- package/dist/scalars.d.ts.map +1 -0
- package/dist/scalars.js +88 -0
- package/dist/schema.d.ts +17 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +130 -0
- package/dist/select.d.ts +24 -0
- package/dist/select.d.ts.map +1 -0
- package/dist/select.js +152 -0
- package/dist/sockets.d.ts +25 -0
- package/dist/sockets.d.ts.map +1 -0
- package/dist/sockets.js +234 -0
- package/dist/subscriptions.d.ts +14 -0
- package/dist/subscriptions.d.ts.map +1 -0
- package/dist/subscriptions.js +242 -0
- package/package.json +58 -3
- package/src/data-types.ts +100 -0
- package/src/data.ts +366 -0
- package/src/depth.ts +98 -0
- package/src/errors.ts +48 -0
- package/src/functions.ts +291 -0
- package/src/graphiql.ts +54 -0
- package/src/index.ts +231 -0
- package/src/inputs.ts +348 -0
- package/src/json-fields.ts +95 -0
- package/src/json-schema.ts +21 -0
- package/src/live-stream.ts +192 -0
- package/src/mutations.ts +317 -0
- package/src/names.ts +25 -0
- package/src/outputs.ts +267 -0
- package/src/overlap.ts +97 -0
- package/src/pages.ts +39 -0
- package/src/registry.ts +189 -0
- package/src/reread.ts +159 -0
- package/src/rows.ts +151 -0
- package/src/scalars.ts +110 -0
- package/src/schema.ts +189 -0
- package/src/select.ts +230 -0
- package/src/sockets.ts +291 -0
- package/src/subscriptions.ts +334 -0
package/dist/inputs.js
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
// The read inputs (D-107), as the query API spells them (D-20): filters,
|
|
2
|
+
// `where`, `orderBy` and the `@oneOf` unique target.
|
|
3
|
+
import { readable } from '@ultimabase/http/internal';
|
|
4
|
+
import { GraphQLBoolean, GraphQLEnumType, GraphQLID, GraphQLInputObjectType, GraphQLList, GraphQLNonNull, GraphQLString, } from 'graphql';
|
|
5
|
+
import { upper } from './names.js';
|
|
6
|
+
import { scalarType } from './registry.js';
|
|
7
|
+
import { DateTime } from './scalars.js';
|
|
8
|
+
/** An input object's value, as the kernel's arguments. */
|
|
9
|
+
export const objectOf = (value) => typeof value === 'object' && value !== null ? { ...value } : {};
|
|
10
|
+
/** Whether a model has fields its `_search` matches (D-148). */
|
|
11
|
+
const searched = (fields) => fields.some((field) => field.kind === 'scalar' && field.search === true);
|
|
12
|
+
const list = (type) => new GraphQLList(new GraphQLNonNull(type));
|
|
13
|
+
const base = ['equals', 'not', 'in', 'notIn'];
|
|
14
|
+
const ordered = [...base, 'lt', 'lte', 'gt', 'gte'];
|
|
15
|
+
const text = [...base, 'contains', 'startsWith', 'endsWith'];
|
|
16
|
+
/** `QueryMode`: a string filter's operators ignore case (D-140). */
|
|
17
|
+
const queryMode = (registry) => registry.enum('QueryMode', () => new GraphQLEnumType({
|
|
18
|
+
name: 'QueryMode',
|
|
19
|
+
values: { insensitive: { value: 'insensitive' } },
|
|
20
|
+
}));
|
|
21
|
+
/** The input type of a filter's operator. */
|
|
22
|
+
function operand(registry, type, operator) {
|
|
23
|
+
if (operator === 'mode')
|
|
24
|
+
return queryMode(registry);
|
|
25
|
+
if (operator === 'path')
|
|
26
|
+
return list(GraphQLString);
|
|
27
|
+
if (operator.startsWith('string'))
|
|
28
|
+
return GraphQLString;
|
|
29
|
+
return operator === 'in' || operator === 'notIn' ? list(type) : type;
|
|
30
|
+
}
|
|
31
|
+
/** `StringFilter`, `FloatFilter`, `PostStatusFilter`: the kernel's operators. */
|
|
32
|
+
function filter(registry, type, operators) {
|
|
33
|
+
const name = `${type.name}Filter`;
|
|
34
|
+
return registry.input(name, () => new GraphQLInputObjectType({
|
|
35
|
+
name,
|
|
36
|
+
fields: Object.fromEntries(operators.map((operator) => [
|
|
37
|
+
operator,
|
|
38
|
+
{ type: operand(registry, type, operator) },
|
|
39
|
+
])),
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
/** Each scalar type's operators: no order on strings, as the kernel. */
|
|
43
|
+
const operators = {
|
|
44
|
+
string: [...text, 'mode'],
|
|
45
|
+
number: ordered,
|
|
46
|
+
decimal: ordered,
|
|
47
|
+
dateTime: ordered,
|
|
48
|
+
date: ordered,
|
|
49
|
+
boolean: base,
|
|
50
|
+
// Any JSON, a path into it or not, typed field or not (D-149).
|
|
51
|
+
json: [
|
|
52
|
+
'path',
|
|
53
|
+
...ordered,
|
|
54
|
+
'stringContains',
|
|
55
|
+
'stringStartsWith',
|
|
56
|
+
'stringEndsWith',
|
|
57
|
+
'mode',
|
|
58
|
+
'arrayContains',
|
|
59
|
+
'arrayStartsWith',
|
|
60
|
+
'arrayEndsWith',
|
|
61
|
+
],
|
|
62
|
+
enum: base,
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* `StringListFilter`, `FloatListFilter`, `ProductSizesListFilter`: no
|
|
66
|
+
* shorthand, `NOT` gives the opposite (D-120, D-127, D-149).
|
|
67
|
+
*/
|
|
68
|
+
const listFilter = (registry, item) => {
|
|
69
|
+
const name = `${item.name}ListFilter`;
|
|
70
|
+
return registry.input(name, () => new GraphQLInputObjectType({
|
|
71
|
+
name,
|
|
72
|
+
fields: {
|
|
73
|
+
has: { type: item },
|
|
74
|
+
hasSome: { type: list(item) },
|
|
75
|
+
hasEvery: { type: list(item) },
|
|
76
|
+
isEmpty: { type: GraphQLBoolean },
|
|
77
|
+
},
|
|
78
|
+
}));
|
|
79
|
+
};
|
|
80
|
+
const scalarFilter = (registry, model, field) => {
|
|
81
|
+
const item = scalarType(registry, model, field);
|
|
82
|
+
if (field.list)
|
|
83
|
+
return listFilter(registry, item);
|
|
84
|
+
return filter(registry, item, operators[field.type]);
|
|
85
|
+
};
|
|
86
|
+
const relationFilter = (registry, field) => {
|
|
87
|
+
const one = field.kind === 'one';
|
|
88
|
+
const name = `${field.target}${one ? 'Relation' : 'ListRelation'}Filter`;
|
|
89
|
+
return registry.input(name, () => new GraphQLInputObjectType({
|
|
90
|
+
name,
|
|
91
|
+
fields: () => {
|
|
92
|
+
const type = whereInput(registry, field.target);
|
|
93
|
+
return Object.fromEntries((one ? ['is', 'isNot'] : ['some', 'every', 'none']).map((key) => [
|
|
94
|
+
key,
|
|
95
|
+
{ type },
|
|
96
|
+
]));
|
|
97
|
+
},
|
|
98
|
+
}));
|
|
99
|
+
};
|
|
100
|
+
/** `PostWhereInput`. */
|
|
101
|
+
export function whereInput(registry, model) {
|
|
102
|
+
const name = `${model}WhereInput`;
|
|
103
|
+
return registry.input(name, () => new GraphQLInputObjectType({
|
|
104
|
+
name,
|
|
105
|
+
fields: () => {
|
|
106
|
+
const self = whereInput(registry, model);
|
|
107
|
+
const fields = {
|
|
108
|
+
AND: { type: list(self) },
|
|
109
|
+
OR: { type: list(self) },
|
|
110
|
+
NOT: { type: list(self) },
|
|
111
|
+
id: { type: filter(registry, GraphQLID, base) },
|
|
112
|
+
createdAt: { type: filter(registry, DateTime, ordered) },
|
|
113
|
+
updatedAt: { type: filter(registry, DateTime, ordered) },
|
|
114
|
+
};
|
|
115
|
+
const entry = registry.entry(model);
|
|
116
|
+
if (searched(readable(entry))) {
|
|
117
|
+
fields['_search'] = { type: GraphQLString };
|
|
118
|
+
}
|
|
119
|
+
for (const field of readable(entry)) {
|
|
120
|
+
// Only a beforeFind filters a plugin field (D-122).
|
|
121
|
+
if (entry.exposed.fieldTypes[field.name] !== undefined)
|
|
122
|
+
continue;
|
|
123
|
+
fields[field.name] = {
|
|
124
|
+
type: field.kind === 'scalar'
|
|
125
|
+
? scalarFilter(registry, model, field)
|
|
126
|
+
: relationFilter(registry, field),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return fields;
|
|
130
|
+
},
|
|
131
|
+
}));
|
|
132
|
+
}
|
|
133
|
+
const sortOrder = (registry) => registry.enum('SortOrder', () => new GraphQLEnumType({
|
|
134
|
+
name: 'SortOrder',
|
|
135
|
+
values: { asc: { value: 'asc' }, desc: { value: 'desc' } },
|
|
136
|
+
}));
|
|
137
|
+
/** `CountOrderByInput`: a `many` relation sorted by its rows (D-134). */
|
|
138
|
+
const countOrderBy = (registry) => registry.input('CountOrderByInput', () => new GraphQLInputObjectType({
|
|
139
|
+
name: 'CountOrderByInput',
|
|
140
|
+
fields: { _count: { type: sortOrder(registry) } },
|
|
141
|
+
}));
|
|
142
|
+
/**
|
|
143
|
+
* `PostOrderByInput`: scalar fields but json, lists and plugin fields, as
|
|
144
|
+
* the kernel sorts (D-94, D-120, D-122); a `one` relation as its target's,
|
|
145
|
+
* a `many` one by its count (D-134).
|
|
146
|
+
*/
|
|
147
|
+
export function orderByInput(registry, model) {
|
|
148
|
+
const name = `${model}OrderByInput`;
|
|
149
|
+
return registry.input(name, () => new GraphQLInputObjectType({
|
|
150
|
+
name,
|
|
151
|
+
fields: () => {
|
|
152
|
+
const type = sortOrder(registry);
|
|
153
|
+
const fields = {
|
|
154
|
+
id: { type },
|
|
155
|
+
createdAt: { type },
|
|
156
|
+
updatedAt: { type },
|
|
157
|
+
};
|
|
158
|
+
const entry = registry.entry(model);
|
|
159
|
+
// The kernel refuses it in a relation's: a read ranks by its own search.
|
|
160
|
+
if (searched(readable(entry)))
|
|
161
|
+
fields['_relevance'] = { type };
|
|
162
|
+
for (const field of readable(entry)) {
|
|
163
|
+
if (field.kind === 'one') {
|
|
164
|
+
fields[field.name] = {
|
|
165
|
+
type: orderByInput(registry, field.target),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
else if (field.kind === 'many') {
|
|
169
|
+
fields[field.name] = { type: countOrderBy(registry) };
|
|
170
|
+
}
|
|
171
|
+
else if (field.type !== 'json' &&
|
|
172
|
+
!field.list &&
|
|
173
|
+
entry.exposed.fieldTypes[field.name] === undefined) {
|
|
174
|
+
fields[field.name] = { type };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return fields;
|
|
178
|
+
},
|
|
179
|
+
}));
|
|
180
|
+
}
|
|
181
|
+
/** A unique field's type: a `one` relation's is its target's `ID`. */
|
|
182
|
+
function uniqueType(registry, model, name) {
|
|
183
|
+
const field = registry.entry(model).model.fields.find((f) => f.name === name);
|
|
184
|
+
return field?.kind === 'scalar'
|
|
185
|
+
? scalarType(registry, model, field)
|
|
186
|
+
: GraphQLID;
|
|
187
|
+
}
|
|
188
|
+
const compositeName = (fields) => fields.join('_');
|
|
189
|
+
/**
|
|
190
|
+
* `PostWhereUniqueInput @oneOf`: `id`, each unique field, and each unique
|
|
191
|
+
* index as `author_slug: { author, slug }`.
|
|
192
|
+
*/
|
|
193
|
+
export function uniqueInput(registry, model) {
|
|
194
|
+
const name = `${model}WhereUniqueInput`;
|
|
195
|
+
return registry.input(name, () => {
|
|
196
|
+
const fields = {};
|
|
197
|
+
const { exposed } = registry.entry(model);
|
|
198
|
+
// A field only Master reads is absent, as from the filters (D-44).
|
|
199
|
+
const uniques = exposed.uniques.filter((unique) => unique.every((field) => exposed.readRules[field] !== 'private'));
|
|
200
|
+
for (const unique of uniques) {
|
|
201
|
+
const [only] = unique;
|
|
202
|
+
if (unique.length === 1 && only !== undefined) {
|
|
203
|
+
fields[only] = { type: uniqueType(registry, model, only) };
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const key = compositeName(unique);
|
|
207
|
+
const composite = `${model}${unique.map(upper).join('')}UniqueInput`;
|
|
208
|
+
fields[key] = {
|
|
209
|
+
type: new GraphQLInputObjectType({
|
|
210
|
+
name: composite,
|
|
211
|
+
fields: Object.fromEntries(unique.map((field) => [
|
|
212
|
+
field,
|
|
213
|
+
{ type: new GraphQLNonNull(uniqueType(registry, model, field)) },
|
|
214
|
+
])),
|
|
215
|
+
}),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
return new GraphQLInputObjectType({ name, fields, isOneOf: true });
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
/** A `@oneOf` unique target as the kernel's flat `where`. */
|
|
222
|
+
export function uniqueWhere(registry, model, where) {
|
|
223
|
+
const composites = new Set(registry
|
|
224
|
+
.entry(model)
|
|
225
|
+
.exposed.uniques.filter((unique) => unique.length > 1)
|
|
226
|
+
.map(compositeName));
|
|
227
|
+
const flat = {};
|
|
228
|
+
for (const [key, value] of Object.entries(where)) {
|
|
229
|
+
if (composites.has(key) && typeof value === 'object') {
|
|
230
|
+
Object.assign(flat, value);
|
|
231
|
+
}
|
|
232
|
+
else
|
|
233
|
+
flat[key] = value;
|
|
234
|
+
}
|
|
235
|
+
return flat;
|
|
236
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { GraphQLNullableInputType, GraphQLNullableOutputType } from 'graphql';
|
|
2
|
+
import type { Typed } from './data.ts';
|
|
3
|
+
import type { Args, Registry, Scalar } from './registry.ts';
|
|
4
|
+
/** `UserPrefs` or `PostCover`, and whether the schema reads `null`. */
|
|
5
|
+
export declare function jsonOutput(registry: Registry, model: string, field: Scalar): Typed<GraphQLNullableOutputType>;
|
|
6
|
+
/** `UserPrefsInput`, and whether the schema takes `null`. */
|
|
7
|
+
export declare function jsonInput(registry: Registry, model: string, field: Scalar): Typed<GraphQLNullableInputType>;
|
|
8
|
+
/** A written row's json and plugin values, their dates back to text. */
|
|
9
|
+
export declare function plainData(registry: Registry, model: string, data: Args): Args;
|
|
10
|
+
/** A filter's json operands, their dates back to text, through relations. */
|
|
11
|
+
export declare function plainWhere(registry: Registry, model: string, where: Args): Args;
|
|
12
|
+
//# sourceMappingURL=json-fields.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json-fields.d.ts","sourceRoot":"","sources":["../src/json-fields.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACX,wBAAwB,EACxB,yBAAyB,EACzB,MAAM,SAAS,CAAA;AAChB,OAAO,KAAK,EAAQ,KAAK,EAAE,MAAM,WAAW,CAAA;AAG5C,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,eAAe,CAAA;AAY3D,uEAAuE;AACvE,wBAAgB,UAAU,CACzB,QAAQ,EAAE,QAAQ,EAClB,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,GACX,KAAK,CAAC,yBAAyB,CAAC,CAKlC;AAED,6DAA6D;AAC7D,wBAAgB,SAAS,CACxB,QAAQ,EAAE,QAAQ,EAClB,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,GACX,KAAK,CAAC,wBAAwB,CAAC,CAIjC;AAED,wEAAwE;AACxE,wBAAgB,SAAS,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG,IAAI,CAW7E;AASD,6EAA6E;AAC7E,wBAAgB,UAAU,CACzB,QAAQ,EAAE,QAAQ,EAClB,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,IAAI,GACT,IAAI,CAsBN"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { fieldInput, fieldOutput } from './data.js';
|
|
2
|
+
import { upper } from './names.js';
|
|
3
|
+
import { JSONScalar, plain } from './scalars.js';
|
|
4
|
+
const pathOf = (model, field, suffix) => ({
|
|
5
|
+
owner: `${model}.${field.name}`,
|
|
6
|
+
name: `${model}${upper(field.name)}`,
|
|
7
|
+
suffix,
|
|
8
|
+
at: field.name,
|
|
9
|
+
});
|
|
10
|
+
const untyped = { type: JSONScalar, nullable: false };
|
|
11
|
+
/** `UserPrefs` or `PostCover`, and whether the schema reads `null`. */
|
|
12
|
+
export function jsonOutput(registry, model, field) {
|
|
13
|
+
const { validators, fieldTypes } = registry.entry(model).exposed;
|
|
14
|
+
const schema = validators[field.name] ?? fieldTypes[field.name]?.shape;
|
|
15
|
+
const path = pathOf(model, field, '');
|
|
16
|
+
return (schema && fieldOutput(schema, path, registry.data)) ?? untyped;
|
|
17
|
+
}
|
|
18
|
+
/** `UserPrefsInput`, and whether the schema takes `null`. */
|
|
19
|
+
export function jsonInput(registry, model, field) {
|
|
20
|
+
const schema = registry.entry(model).exposed.validators[field.name];
|
|
21
|
+
const path = pathOf(model, field, 'Input');
|
|
22
|
+
return (schema && fieldInput(schema, path, registry.data)) ?? untyped;
|
|
23
|
+
}
|
|
24
|
+
/** A written row's json and plugin values, their dates back to text. */
|
|
25
|
+
export function plainData(registry, model, data) {
|
|
26
|
+
const plained = { ...data };
|
|
27
|
+
const { exposed, model: schema } = registry.entry(model);
|
|
28
|
+
for (const field of schema.fields) {
|
|
29
|
+
const value = plained[field.name];
|
|
30
|
+
if (value === undefined || field.kind !== 'scalar')
|
|
31
|
+
continue;
|
|
32
|
+
if (field.type === 'json' || exposed.fieldTypes[field.name] !== undefined) {
|
|
33
|
+
plained[field.name] = plain(value);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return plained;
|
|
37
|
+
}
|
|
38
|
+
const each = (value, map) => Array.isArray(value)
|
|
39
|
+
? value.map((item) => each(item, map))
|
|
40
|
+
: typeof value === 'object' && value !== null
|
|
41
|
+
? map({ ...value })
|
|
42
|
+
: value;
|
|
43
|
+
/** A filter's json operands, their dates back to text, through relations. */
|
|
44
|
+
export function plainWhere(registry, model, where) {
|
|
45
|
+
const fields = registry.entry(model).model.fields;
|
|
46
|
+
const plained = {};
|
|
47
|
+
for (const [key, value] of Object.entries(where)) {
|
|
48
|
+
const field = fields.find((candidate) => candidate.name === key);
|
|
49
|
+
if (key === 'AND' || key === 'OR' || key === 'NOT') {
|
|
50
|
+
plained[key] = each(value, (inner) => plainWhere(registry, model, inner));
|
|
51
|
+
}
|
|
52
|
+
else if (field?.kind === 'scalar') {
|
|
53
|
+
plained[key] = field.type === 'json' ? plain(value) : value;
|
|
54
|
+
}
|
|
55
|
+
else if (field !== undefined) {
|
|
56
|
+
// `is`, `some` and the others each take the target's filter.
|
|
57
|
+
plained[key] = each(value, (operators) => Object.fromEntries(Object.entries(operators).map(([operator, inner]) => [
|
|
58
|
+
operator,
|
|
59
|
+
each(inner, (target) => plainWhere(registry, field.target, target)),
|
|
60
|
+
])));
|
|
61
|
+
}
|
|
62
|
+
else
|
|
63
|
+
plained[key] = value;
|
|
64
|
+
}
|
|
65
|
+
return plained;
|
|
66
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Node } from '@ultimabase/http/internal';
|
|
2
|
+
export type Side = 'input' | 'output';
|
|
3
|
+
/** A json field's JSON Schema, `undefined` without one (D-125). */
|
|
4
|
+
export declare function exportOf(schema: object, side: Side): Node | undefined;
|
|
5
|
+
/** The JSON Schema's shared nodes, by id. */
|
|
6
|
+
export declare const defsOf: (root: Node) => Node;
|
|
7
|
+
//# sourceMappingURL=json-schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json-schema.d.ts","sourceRoot":"","sources":["../src/json-schema.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,2BAA2B,CAAA;AAIrD,MAAM,MAAM,IAAI,GAAG,OAAO,GAAG,QAAQ,CAAA;AAErC,mEAAmE;AACnE,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG,IAAI,GAAG,SAAS,CAOrE;AAED,6CAA6C;AAC7C,eAAO,MAAM,MAAM,SAAU,IAAI,KAAG,IACO,CAAA"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// A schema's standard JSON Schema export, which `data.ts` types (D-45).
|
|
2
|
+
import { isNode, jsonSchemaOf } from '@ultimabase/http/internal';
|
|
3
|
+
import { Refused } from 'ultimabase';
|
|
4
|
+
/** A json field's JSON Schema, `undefined` without one (D-125). */
|
|
5
|
+
export function exportOf(schema, side) {
|
|
6
|
+
try {
|
|
7
|
+
return jsonSchemaOf(schema, side, '');
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
if (error instanceof Refused)
|
|
11
|
+
return undefined;
|
|
12
|
+
throw error;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** The JSON Schema's shared nodes, by id. */
|
|
16
|
+
export const defsOf = (root) => isNode(root['$defs']) ? root['$defs'] : {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { ExecutionResult } from 'graphql';
|
|
2
|
+
import type { LiveHandle, LiveMessage, Viewer } from '@ultimabase/realtime';
|
|
3
|
+
/** A message to run against the selection, and how its result is sent. */
|
|
4
|
+
export declare class LiveEvent {
|
|
5
|
+
readonly root: unknown;
|
|
6
|
+
readonly finish: (result: ExecutionResult) => ExecutionResult;
|
|
7
|
+
constructor(root: unknown, finish: (result: ExecutionResult) => ExecutionResult);
|
|
8
|
+
}
|
|
9
|
+
/** The socket a connection's subscriptions write to. */
|
|
10
|
+
export type Channel = {
|
|
11
|
+
readonly viewer: Viewer;
|
|
12
|
+
/** Whether the client's buffer holds more than it may. */
|
|
13
|
+
readonly full: () => boolean;
|
|
14
|
+
/** Runs `then` once everything sent is written. */
|
|
15
|
+
readonly drained: (then: () => void) => void;
|
|
16
|
+
/** Closes the socket: its caller is no longer the same user. */
|
|
17
|
+
readonly forbid: () => void;
|
|
18
|
+
/** The completed live lists a resume may take back, by token. */
|
|
19
|
+
readonly kept: Map<string, Kept>;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* A subscription's channel, whether it gets the whole window each time, and
|
|
23
|
+
* the token of the completed list it resumes.
|
|
24
|
+
*/
|
|
25
|
+
export type Live = {
|
|
26
|
+
readonly channel: Channel;
|
|
27
|
+
readonly page: boolean;
|
|
28
|
+
readonly resume?: string;
|
|
29
|
+
};
|
|
30
|
+
/** A live list's token, what read it is, and the rows it shows. */
|
|
31
|
+
export type Keep = {
|
|
32
|
+
readonly token: string;
|
|
33
|
+
readonly signature: string;
|
|
34
|
+
first: number;
|
|
35
|
+
};
|
|
36
|
+
/** A completed live list, until a resume takes it or its time is up. */
|
|
37
|
+
export type Kept = {
|
|
38
|
+
readonly keep: Keep;
|
|
39
|
+
readonly stream: AsyncIterableIterator<LiveEvent>;
|
|
40
|
+
readonly handle: LiveHandle;
|
|
41
|
+
readonly timer: NodeJS.Timeout;
|
|
42
|
+
readonly end: () => void;
|
|
43
|
+
};
|
|
44
|
+
/** Each WebSocket operation's, by its context. */
|
|
45
|
+
export declare const lives: WeakMap<object, Live>;
|
|
46
|
+
/**
|
|
47
|
+
* The list `token` keeps, taken back when it is the same read and grown to
|
|
48
|
+
* `first` rows; a resume that grows past a limit ends it.
|
|
49
|
+
*/
|
|
50
|
+
export declare function resumed(channel: Channel, token: string, signature: string, first: number): Promise<AsyncIterableIterator<LiveEvent> | undefined>;
|
|
51
|
+
/**
|
|
52
|
+
* Follows the read `open` starts; `event` turns each message the client
|
|
53
|
+
* gets into what it runs, and `closed`, given a live function's error, into
|
|
54
|
+
* the last one, unless the viewer ended, which closes the socket. A list given `keep` is kept once completed.
|
|
55
|
+
*/
|
|
56
|
+
export declare function liveStream(channel: Channel, open: (send: (message: LiveMessage) => void) => Promise<LiveHandle>, event: (message: Exclude<LiveMessage, {
|
|
57
|
+
kind: 'closed';
|
|
58
|
+
}>) => LiveEvent, closed: (error?: unknown) => LiveEvent, keep?: Keep): Promise<AsyncIterableIterator<LiveEvent>>;
|
|
59
|
+
//# sourceMappingURL=live-stream.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"live-stream.d.ts","sourceRoot":"","sources":["../src/live-stream.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAC9C,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAA;AAE3E,0EAA0E;AAC1E,qBAAa,SAAS;IACrB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;IACtB,QAAQ,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,eAAe,CAAA;IAE7D,YACC,IAAI,EAAE,OAAO,EACb,MAAM,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,eAAe,EAIpD;CACD;AAED,wDAAwD;AACxD,MAAM,MAAM,OAAO,GAAG;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,0DAA0D;IAC1D,QAAQ,CAAC,IAAI,EAAE,MAAM,OAAO,CAAA;IAC5B,mDAAmD;IACnD,QAAQ,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,IAAI,KAAK,IAAI,CAAA;IAC5C,gEAAgE;IAChE,QAAQ,CAAC,MAAM,EAAE,MAAM,IAAI,CAAA;IAC3B,iEAAiE;IACjE,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;CAChC,CAAA;AAED;;;GAGG;AACH,MAAM,MAAM,IAAI,GAAG;IAClB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;IACtB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CACxB,CAAA;AAED,mEAAmE;AACnE,MAAM,MAAM,IAAI,GAAG;IAClB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,KAAK,EAAE,MAAM,CAAA;CACb,CAAA;AAED,wEAAwE;AACxE,MAAM,MAAM,IAAI,GAAG;IAClB,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAA;IACnB,QAAQ,CAAC,MAAM,EAAE,qBAAqB,CAAC,SAAS,CAAC,CAAA;IACjD,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAA;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,OAAO,CAAA;IAC9B,QAAQ,CAAC,GAAG,EAAE,MAAM,IAAI,CAAA;CACxB,CAAA;AAKD,kDAAkD;AAClD,eAAO,MAAM,KAAK,EAAE,OAAO,CAAC,MAAM,EAAE,IAAI,CAAiB,CAAA;AAOzD;;;GAGG;AACH,wBAAsB,OAAO,CAC5B,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,GACX,OAAO,CAAC,qBAAqB,CAAC,SAAS,CAAC,GAAG,SAAS,CAAC,CAevD;AAED;;;;GAIG;AACH,wBAAsB,UAAU,CAC/B,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,KAAK,OAAO,CAAC,UAAU,CAAC,EACnE,KAAK,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,WAAW,EAAE;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAC,KAAK,SAAS,EACvE,MAAM,EAAE,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,SAAS,EACtC,IAAI,CAAC,EAAE,IAAI,GACT,OAAO,CAAC,qBAAqB,CAAC,SAAS,CAAC,CAAC,CAmF3C"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/** A message to run against the selection, and how its result is sent. */
|
|
2
|
+
export class LiveEvent {
|
|
3
|
+
root;
|
|
4
|
+
finish;
|
|
5
|
+
constructor(root, finish) {
|
|
6
|
+
this.root = root;
|
|
7
|
+
this.finish = finish;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/** How long a completed live list waits for a resume. */
|
|
11
|
+
const keptFor = 10_000;
|
|
12
|
+
/** Each WebSocket operation's, by its context. */
|
|
13
|
+
export const lives = new WeakMap();
|
|
14
|
+
const finished = () => ({
|
|
15
|
+
done: true,
|
|
16
|
+
value: undefined,
|
|
17
|
+
});
|
|
18
|
+
/**
|
|
19
|
+
* The list `token` keeps, taken back when it is the same read and grown to
|
|
20
|
+
* `first` rows; a resume that grows past a limit ends it.
|
|
21
|
+
*/
|
|
22
|
+
export async function resumed(channel, token, signature, first) {
|
|
23
|
+
const kept = channel.kept.get(token);
|
|
24
|
+
if (kept?.keep.signature !== signature || first < kept.keep.first) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
channel.kept.delete(token);
|
|
28
|
+
clearTimeout(kept.timer);
|
|
29
|
+
try {
|
|
30
|
+
if (first > kept.keep.first)
|
|
31
|
+
await kept.handle.grow(first - kept.keep.first);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
kept.end();
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
kept.keep.first = first;
|
|
38
|
+
return kept.stream;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Follows the read `open` starts; `event` turns each message the client
|
|
42
|
+
* gets into what it runs, and `closed`, given a live function's error, into
|
|
43
|
+
* the last one, unless the viewer ended, which closes the socket. A list given `keep` is kept once completed.
|
|
44
|
+
*/
|
|
45
|
+
export async function liveStream(channel, open, event, closed, keep) {
|
|
46
|
+
const queue = [];
|
|
47
|
+
let waiting;
|
|
48
|
+
let ending = false;
|
|
49
|
+
let done = false;
|
|
50
|
+
// Changes are dropped from a full buffer until the window comes whole.
|
|
51
|
+
let dropping = false;
|
|
52
|
+
let owed = false;
|
|
53
|
+
let handle;
|
|
54
|
+
const push = (value) => {
|
|
55
|
+
const taker = waiting;
|
|
56
|
+
waiting = undefined;
|
|
57
|
+
if (taker === undefined)
|
|
58
|
+
queue.push(value);
|
|
59
|
+
else
|
|
60
|
+
taker({ done: false, value });
|
|
61
|
+
};
|
|
62
|
+
const end = () => {
|
|
63
|
+
done = true;
|
|
64
|
+
queue.length = 0;
|
|
65
|
+
handle?.close();
|
|
66
|
+
const taker = waiting;
|
|
67
|
+
waiting = undefined;
|
|
68
|
+
taker?.(finished());
|
|
69
|
+
};
|
|
70
|
+
const send = (message) => {
|
|
71
|
+
if (done || ending)
|
|
72
|
+
return;
|
|
73
|
+
if (message.kind === 'closed') {
|
|
74
|
+
if (channel.viewer.closed) {
|
|
75
|
+
end();
|
|
76
|
+
return channel.forbid();
|
|
77
|
+
}
|
|
78
|
+
ending = true;
|
|
79
|
+
return push(closed(message.error));
|
|
80
|
+
}
|
|
81
|
+
if (dropping && message.kind === 'changes')
|
|
82
|
+
return;
|
|
83
|
+
if (!dropping && channel.full()) {
|
|
84
|
+
queue.length = 0;
|
|
85
|
+
dropping = true;
|
|
86
|
+
channel.drained(() => {
|
|
87
|
+
if (handle === undefined)
|
|
88
|
+
owed = true;
|
|
89
|
+
else
|
|
90
|
+
void handle.resend();
|
|
91
|
+
});
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
dropping = false;
|
|
95
|
+
push(event(message));
|
|
96
|
+
};
|
|
97
|
+
const opened = await open(send);
|
|
98
|
+
handle = opened;
|
|
99
|
+
if (owed)
|
|
100
|
+
void opened.resend();
|
|
101
|
+
const stream = {
|
|
102
|
+
next: async () => {
|
|
103
|
+
const value = queue.shift();
|
|
104
|
+
if (value !== undefined)
|
|
105
|
+
return { done: false, value };
|
|
106
|
+
if (done || ending) {
|
|
107
|
+
end();
|
|
108
|
+
return finished();
|
|
109
|
+
}
|
|
110
|
+
return new Promise((taker) => {
|
|
111
|
+
waiting = taker;
|
|
112
|
+
});
|
|
113
|
+
},
|
|
114
|
+
return: async () => {
|
|
115
|
+
if (channel.kept.get(keep?.token ?? '')?.stream === stream) {
|
|
116
|
+
return finished();
|
|
117
|
+
}
|
|
118
|
+
if (keep === undefined || done || ending || channel.viewer.closed) {
|
|
119
|
+
end();
|
|
120
|
+
return finished();
|
|
121
|
+
}
|
|
122
|
+
const taker = waiting;
|
|
123
|
+
waiting = undefined;
|
|
124
|
+
taker?.(finished());
|
|
125
|
+
const timer = setTimeout(() => {
|
|
126
|
+
channel.kept.delete(keep.token);
|
|
127
|
+
end();
|
|
128
|
+
}, keptFor);
|
|
129
|
+
timer.unref();
|
|
130
|
+
channel.kept.set(keep.token, { keep, stream, handle: opened, timer, end });
|
|
131
|
+
return finished();
|
|
132
|
+
},
|
|
133
|
+
[Symbol.asyncIterator]: () => stream,
|
|
134
|
+
};
|
|
135
|
+
return stream;
|
|
136
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { GraphQLFieldConfig } from 'graphql';
|
|
2
|
+
import type { Args, Context, Entry, Registry } from './registry.ts';
|
|
3
|
+
type Fields = {
|
|
4
|
+
[field: string]: GraphQLFieldConfig<unknown, Context, Args>;
|
|
5
|
+
};
|
|
6
|
+
/** A visible model's four mutations. */
|
|
7
|
+
export declare function mutationFields(registry: Registry, entry: Entry): Fields;
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=mutations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mutations.d.ts","sourceRoot":"","sources":["../src/mutations.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACX,kBAAkB,EAGlB,MAAM,SAAS,CAAA;AAahB,OAAO,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAKnE,KAAK,MAAM,GAAG;IAAE,CAAC,KAAK,EAAE,MAAM,GAAG,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,CAAA;CAAE,CAAA;AA2N7E,wCAAwC;AACxC,wBAAgB,cAAc,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,GAAG,MAAM,CAoEvE"}
|