@ultimabase/graphql 0.0.0-stage → 0.1.1
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 +24 -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 +53 -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
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
// The writes (D-107): `createPost(data)`, `updatePost(where, data)`,
|
|
2
|
+
// `upsertPost(where, create, update)` (D-142) and `deletePost(where)`, each
|
|
3
|
+
// one kernel write run as its caller. A create, an update and an upsert
|
|
4
|
+
// return the row as the query asks for it, a delete the row as it was,
|
|
5
|
+
// without its relations.
|
|
6
|
+
import { run } from '@ultimabase/http/internal';
|
|
7
|
+
import { GraphQLBoolean, GraphQLID, GraphQLInputObjectType, GraphQLList, GraphQLNonNull, } from 'graphql';
|
|
8
|
+
import { objectOf, uniqueInput, uniqueWhere } from './inputs.js';
|
|
9
|
+
import { jsonInput, plainData } from './json-fields.js';
|
|
10
|
+
import { upper } from './names.js';
|
|
11
|
+
import { deletedType, isMaster, objectType } from './outputs.js';
|
|
12
|
+
import { as, described, valueType } from './registry.js';
|
|
13
|
+
import { JSONScalar } from './scalars.js';
|
|
14
|
+
import { selectOf } from './select.js';
|
|
15
|
+
/** `ConnectInput { id }`: what a relation write names a row by. */
|
|
16
|
+
const connectInput = (registry) => registry.input('ConnectInput', () => new GraphQLInputObjectType({
|
|
17
|
+
name: 'ConnectInput',
|
|
18
|
+
fields: { id: { type: new GraphQLNonNull(GraphQLID) } },
|
|
19
|
+
}));
|
|
20
|
+
const facing = (field) => `Without${upper(field.inverse)}`;
|
|
21
|
+
/** `CommentCreateOrConnectWithoutPostInput { where, create }` (D-138). */
|
|
22
|
+
const orCreateInput = (registry, field) => {
|
|
23
|
+
const name = `${field.target}CreateOrConnect${facing(field)}Input`;
|
|
24
|
+
return registry.input(name, () => new GraphQLInputObjectType({
|
|
25
|
+
name,
|
|
26
|
+
fields: () => ({
|
|
27
|
+
where: {
|
|
28
|
+
type: new GraphQLNonNull(uniqueInput(registry, field.target)),
|
|
29
|
+
},
|
|
30
|
+
create: {
|
|
31
|
+
type: new GraphQLNonNull(dataInput(registry, field.target, true, field.inverse)),
|
|
32
|
+
},
|
|
33
|
+
}),
|
|
34
|
+
}));
|
|
35
|
+
};
|
|
36
|
+
/** `CommentUpdateWithWhereUniqueWithoutPostInput { where, data }` (D-138). */
|
|
37
|
+
const updateWhereInput = (registry, field) => {
|
|
38
|
+
const name = `${field.target}UpdateWithWhereUnique${facing(field)}Input`;
|
|
39
|
+
return registry.input(name, () => new GraphQLInputObjectType({
|
|
40
|
+
name,
|
|
41
|
+
fields: () => ({
|
|
42
|
+
where: {
|
|
43
|
+
type: new GraphQLNonNull(uniqueInput(registry, field.target)),
|
|
44
|
+
},
|
|
45
|
+
data: {
|
|
46
|
+
type: new GraphQLNonNull(dataInput(registry, field.target, false, field.inverse)),
|
|
47
|
+
},
|
|
48
|
+
}),
|
|
49
|
+
}));
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* A relation's write, named as Prisma names it (D-20, D-128, D-138): a
|
|
53
|
+
* create connects or creates, an update also disconnects, unless the
|
|
54
|
+
* relation is a required `one`, and updates or deletes a linked row; a
|
|
55
|
+
* `many` is also `set`. `create` takes the target's data without the
|
|
56
|
+
* relation back: `PostCreateInput.comments` is
|
|
57
|
+
* `CommentCreateNestedManyWithoutPostInput { connect, connectOrCreate,
|
|
58
|
+
* create: [CommentCreateWithoutPostInput!] }`. A hidden model is not
|
|
59
|
+
* created, updated or deleted over GraphQL, through a relation either, as
|
|
60
|
+
* in Parse.
|
|
61
|
+
*/
|
|
62
|
+
function relationInput(registry, field, creating) {
|
|
63
|
+
const many = field.kind === 'many';
|
|
64
|
+
const shape = many ? 'Many' : field.optional ? 'One' : 'OneRequired';
|
|
65
|
+
const name = creating
|
|
66
|
+
? `${field.target}CreateNested${many ? 'Many' : 'One'}${facing(field)}Input`
|
|
67
|
+
: `${field.target}Update${shape}${facing(field)}NestedInput`;
|
|
68
|
+
return registry.input(name, () => {
|
|
69
|
+
const listed = (type) => many ? new GraphQLList(new GraphQLNonNull(type)) : type;
|
|
70
|
+
const row = listed(connectInput(registry));
|
|
71
|
+
const visible = registry.entry(field.target).visible;
|
|
72
|
+
const updating = !creating && visible;
|
|
73
|
+
const fields = { connect: { type: row } };
|
|
74
|
+
if (visible) {
|
|
75
|
+
fields['connectOrCreate'] = {
|
|
76
|
+
type: listed(orCreateInput(registry, field)),
|
|
77
|
+
};
|
|
78
|
+
const created = dataInput(registry, field.target, true, field.inverse);
|
|
79
|
+
fields['create'] = { type: listed(created) };
|
|
80
|
+
}
|
|
81
|
+
if (updating && shape !== 'OneRequired') {
|
|
82
|
+
const unique = uniqueInput(registry, field.target);
|
|
83
|
+
fields['delete'] = { type: many ? listed(unique) : GraphQLBoolean };
|
|
84
|
+
}
|
|
85
|
+
if (!creating && shape !== 'OneRequired') {
|
|
86
|
+
fields['disconnect'] = { type: many ? row : GraphQLBoolean };
|
|
87
|
+
}
|
|
88
|
+
// A filtered relation takes no `set` (D-151).
|
|
89
|
+
if (!creating && many && field.filtered !== true) {
|
|
90
|
+
fields['set'] = { type: row };
|
|
91
|
+
}
|
|
92
|
+
if (updating) {
|
|
93
|
+
fields['update'] = {
|
|
94
|
+
type: many
|
|
95
|
+
? listed(updateWhereInput(registry, field))
|
|
96
|
+
: dataInput(registry, field.target, false, field.inverse),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
// A `one` relation takes one of them.
|
|
100
|
+
return new GraphQLInputObjectType({ name, fields, isOneOf: !many });
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The kernel's data from a mutation's: its json values plain, and each
|
|
105
|
+
* nested row's data and unique target too (D-128, D-138).
|
|
106
|
+
*/
|
|
107
|
+
function kernelData(registry, model, data) {
|
|
108
|
+
const plained = plainData(registry, model, data);
|
|
109
|
+
for (const field of registry.entry(model).model.fields) {
|
|
110
|
+
const value = plained[field.name];
|
|
111
|
+
if (field.kind === 'scalar' || typeof value !== 'object' || value === null)
|
|
112
|
+
continue;
|
|
113
|
+
const { target } = field;
|
|
114
|
+
const row = (item) => kernelData(registry, target, objectOf(item));
|
|
115
|
+
const unique = (item) => uniqueWhere(registry, target, objectOf(item));
|
|
116
|
+
const write = { ...value };
|
|
117
|
+
const map = (key, read) => {
|
|
118
|
+
const operand = write[key];
|
|
119
|
+
if (operand === undefined || operand === null)
|
|
120
|
+
return;
|
|
121
|
+
write[key] = Array.isArray(operand) ? operand.map(read) : read(operand);
|
|
122
|
+
};
|
|
123
|
+
map('create', row);
|
|
124
|
+
map('connectOrCreate', (item) => {
|
|
125
|
+
const { where, create } = objectOf(item);
|
|
126
|
+
return { where: unique(where), create: row(create) };
|
|
127
|
+
});
|
|
128
|
+
if (field.kind === 'many') {
|
|
129
|
+
map('update', (item) => {
|
|
130
|
+
const { where, data: changes } = objectOf(item);
|
|
131
|
+
return { where: unique(where), data: row(changes) };
|
|
132
|
+
});
|
|
133
|
+
map('delete', unique);
|
|
134
|
+
}
|
|
135
|
+
else
|
|
136
|
+
map('update', row);
|
|
137
|
+
plained[field.name] = write;
|
|
138
|
+
}
|
|
139
|
+
return plained;
|
|
140
|
+
}
|
|
141
|
+
function fieldInput(registry, model, field, creating) {
|
|
142
|
+
if (field.kind === 'scalar' && field.type === 'json') {
|
|
143
|
+
return jsonInput(registry, model, field).type;
|
|
144
|
+
}
|
|
145
|
+
// A plugin field's schema takes its JSON (D-122).
|
|
146
|
+
if (registry.entry(model).exposed.fieldTypes[field.name] !== undefined) {
|
|
147
|
+
return JSONScalar;
|
|
148
|
+
}
|
|
149
|
+
if (field.kind === 'scalar')
|
|
150
|
+
return valueType(registry, model, field);
|
|
151
|
+
return relationInput(registry, field, creating);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* `PostCreateInput` and `PostUpdateInput`: every field, those only Master
|
|
155
|
+
* reads included. A create needs the fields its type needs, neither optional
|
|
156
|
+
* nor with a default (D-53); the others are optional. A row created or
|
|
157
|
+
* updated through a relation takes `CommentCreateWithoutPostInput` or
|
|
158
|
+
* `CommentUpdateWithoutPostInput`, without that relation (D-128, D-138).
|
|
159
|
+
*/
|
|
160
|
+
function dataInput(registry, model, creating, without) {
|
|
161
|
+
const name = `${model}${creating ? 'Create' : 'Update'}${without === undefined ? '' : `Without${upper(without)}`}Input`;
|
|
162
|
+
return registry.input(name, () => new GraphQLInputObjectType({
|
|
163
|
+
name,
|
|
164
|
+
fields: () => {
|
|
165
|
+
const { exposed } = registry.entry(model);
|
|
166
|
+
const required = new Set(creating ? exposed.required : []);
|
|
167
|
+
const fields = {};
|
|
168
|
+
for (const field of exposed.schema.fields) {
|
|
169
|
+
// A view is read-only (D-151).
|
|
170
|
+
const view = field.kind === 'many' && field.from !== undefined;
|
|
171
|
+
if (field.name === without || view)
|
|
172
|
+
continue;
|
|
173
|
+
const type = fieldInput(registry, model, field, creating);
|
|
174
|
+
fields[field.name] = {
|
|
175
|
+
type: required.has(field.name) ? new GraphQLNonNull(type) : type,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
return described(registry.entry(model), fields);
|
|
179
|
+
},
|
|
180
|
+
}));
|
|
181
|
+
}
|
|
182
|
+
/** A visible model's four mutations. */
|
|
183
|
+
export function mutationFields(registry, entry) {
|
|
184
|
+
const name = entry.model.name;
|
|
185
|
+
const type = objectType(registry, name);
|
|
186
|
+
const { queries } = entry.exposed;
|
|
187
|
+
const where = { type: new GraphQLNonNull(uniqueInput(registry, name)) };
|
|
188
|
+
const data = (creating) => ({
|
|
189
|
+
type: new GraphQLNonNull(dataInput(registry, name, creating)),
|
|
190
|
+
});
|
|
191
|
+
const written = (write) => async (_, args, context, info) => {
|
|
192
|
+
const select = selectOf(registry, info, type, info.fieldNodes, isMaster(context));
|
|
193
|
+
return run(() => write(args, select, context));
|
|
194
|
+
};
|
|
195
|
+
const unique = (args) => uniqueWhere(registry, name, objectOf(args['where']));
|
|
196
|
+
const dataOf = (args) => kernelData(registry, name, objectOf(args['data']));
|
|
197
|
+
return {
|
|
198
|
+
[`create${name}`]: {
|
|
199
|
+
type: new GraphQLNonNull(type),
|
|
200
|
+
args: { data: data(true) },
|
|
201
|
+
resolve: written(async (args, select, context) => queries.create({ data: dataOf(args), select }, as(context))),
|
|
202
|
+
},
|
|
203
|
+
[`update${name}`]: {
|
|
204
|
+
type: new GraphQLNonNull(type),
|
|
205
|
+
args: { where, data: data(false) },
|
|
206
|
+
resolve: written(async (args, select, context) => queries.update({ where: unique(args), data: dataOf(args), select }, as(context))),
|
|
207
|
+
},
|
|
208
|
+
// The row it created or updated, as either branch returns it (D-142).
|
|
209
|
+
[`upsert${name}`]: {
|
|
210
|
+
type: new GraphQLNonNull(type),
|
|
211
|
+
args: { where, create: data(true), update: data(false) },
|
|
212
|
+
resolve: written(async (args, select, context) => {
|
|
213
|
+
const { row } = await entry.exposed.upsert({
|
|
214
|
+
where: unique(args),
|
|
215
|
+
create: kernelData(registry, name, objectOf(args['create'])),
|
|
216
|
+
update: kernelData(registry, name, objectOf(args['update'])),
|
|
217
|
+
select,
|
|
218
|
+
}, as(context));
|
|
219
|
+
return row;
|
|
220
|
+
}),
|
|
221
|
+
},
|
|
222
|
+
[`delete${name}`]: {
|
|
223
|
+
type: new GraphQLNonNull(deletedType(registry, name)),
|
|
224
|
+
args: { where },
|
|
225
|
+
resolve: async (_, args, context) => run(async () => queries.delete({ where: unique(args) }, as(context))),
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
package/dist/names.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const upper: (name: string) => string;
|
|
2
|
+
export declare const lower: (name: string) => string;
|
|
3
|
+
/** A model's root fields: its one-row name and its list name. */
|
|
4
|
+
export declare function rootNames(model: string, given: string | undefined): {
|
|
5
|
+
readonly single: string;
|
|
6
|
+
readonly list: string;
|
|
7
|
+
};
|
|
8
|
+
//# sourceMappingURL=names.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"names.d.ts","sourceRoot":"","sources":["../src/names.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,KAAK,SAAU,MAAM,KAAG,MACQ,CAAA;AAE7C,eAAO,MAAM,KAAK,SAAU,MAAM,KAAG,MACQ,CAAA;AAE7C,iEAAiE;AACjE,wBAAgB,SAAS,CACxB,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,GAAG,SAAS,GACvB;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CASpD"}
|
package/dist/names.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// The names the schema gives each model's types and root fields (D-107).
|
|
2
|
+
import { plural } from '@ultimabase/http/internal';
|
|
3
|
+
import { Refused } from 'ultimabase';
|
|
4
|
+
export const upper = (name) => name.charAt(0).toUpperCase() + name.slice(1);
|
|
5
|
+
export const lower = (name) => name.charAt(0).toLowerCase() + name.slice(1);
|
|
6
|
+
/** A model's root fields: its one-row name and its list name. */
|
|
7
|
+
export function rootNames(model, given) {
|
|
8
|
+
const single = lower(model);
|
|
9
|
+
const list = given ?? plural(single);
|
|
10
|
+
if (list === single) {
|
|
11
|
+
throw new Refused(`${model}: its list field would be named ${single} too; set .with({ graphql: { plural } }).`);
|
|
12
|
+
}
|
|
13
|
+
return { single, list };
|
|
14
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { GraphQLFieldConfigArgumentMap } from 'graphql';
|
|
2
|
+
import { GraphQLObjectType } from 'graphql';
|
|
3
|
+
import type { Context, Registry } from './registry.ts';
|
|
4
|
+
export declare const isMaster: (context: Context) => boolean;
|
|
5
|
+
/** `first`, `after`, `last`, `before`, or `take` and `skip`; then filters. */
|
|
6
|
+
export declare function listArgs(registry: Registry, model: string): GraphQLFieldConfigArgumentMap;
|
|
7
|
+
/**
|
|
8
|
+
* `PostConnection` at the root, `UserPostsConnection` on a relation, each
|
|
9
|
+
* with `totalCount` (D-107, D-134).
|
|
10
|
+
*/
|
|
11
|
+
export declare function connectionType(registry: Registry, name: string, model: string): GraphQLObjectType;
|
|
12
|
+
/** `Post`: a field only Master reads is absent, a ruled one nullable. */
|
|
13
|
+
export declare function objectType(registry: Registry, model: string): GraphQLObjectType;
|
|
14
|
+
/** `DeletedPost`: a deleted row as it was, without its relations (D-102). */
|
|
15
|
+
export declare function deletedType(registry: Registry, model: string): GraphQLObjectType;
|
|
16
|
+
//# sourceMappingURL=outputs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"outputs.d.ts","sourceRoot":"","sources":["../src/outputs.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAEX,6BAA6B,EAE7B,MAAM,SAAS,CAAA;AAChB,OAAO,EAMN,iBAAiB,EAEjB,MAAM,SAAS,CAAA;AAMhB,OAAO,KAAK,EAAQ,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAO5D,eAAO,MAAM,QAAQ,YAAa,OAAO,KAAG,OACX,CAAA;AAEjC,8EAA8E;AAC9E,wBAAgB,QAAQ,CACvB,QAAQ,EAAE,QAAQ,EAClB,KAAK,EAAE,MAAM,GACX,6BAA6B,CAiB/B;AA8BD;;;GAGG;AACH,wBAAgB,cAAc,CAC7B,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,GACX,iBAAiB,CAkBnB;AA6GD,yEAAyE;AACzE,wBAAgB,UAAU,CACzB,QAAQ,EAAE,QAAQ,EAClB,KAAK,EAAE,MAAM,GACX,iBAAiB,CA2BnB;AAED,6EAA6E;AAC7E,wBAAgB,WAAW,CAC1B,QAAQ,EAAE,QAAQ,EAClB,KAAK,EAAE,MAAM,GACX,iBAAiB,CAWnB"}
|
package/dist/outputs.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// The output types (D-107): each model's object type, its edges and
|
|
2
|
+
// connections, and the arguments of its lists.
|
|
3
|
+
import { connection, readable, rowsOf } from '@ultimabase/http/internal';
|
|
4
|
+
import { GraphQLBoolean, GraphQLID, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLString, } from 'graphql';
|
|
5
|
+
import { orderByInput, whereInput } from './inputs.js';
|
|
6
|
+
import { upper } from './names.js';
|
|
7
|
+
import { listRead } from './pages.js';
|
|
8
|
+
import { jsonOutput } from './json-fields.js';
|
|
9
|
+
import { described, valueType } from './registry.js';
|
|
10
|
+
import { DateTime } from './scalars.js';
|
|
11
|
+
const required = (type) => new GraphQLNonNull(type);
|
|
12
|
+
export const isMaster = (context) => context.caller.kind === 'master';
|
|
13
|
+
/** `first`, `after`, `last`, `before`, or `take` and `skip`; then filters. */
|
|
14
|
+
export function listArgs(registry, model) {
|
|
15
|
+
const pages = registry.settings.pagination === 'list'
|
|
16
|
+
? { take: { type: GraphQLInt }, skip: { type: GraphQLInt } }
|
|
17
|
+
: {
|
|
18
|
+
first: { type: GraphQLInt },
|
|
19
|
+
after: { type: GraphQLString },
|
|
20
|
+
last: { type: GraphQLInt },
|
|
21
|
+
before: { type: GraphQLString },
|
|
22
|
+
};
|
|
23
|
+
return {
|
|
24
|
+
...pages,
|
|
25
|
+
where: { type: whereInput(registry, model) },
|
|
26
|
+
orderBy: {
|
|
27
|
+
type: new GraphQLList(new GraphQLNonNull(orderByInput(registry, model))),
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const pageInfo = (registry) => registry.object('PageInfo', () => new GraphQLObjectType({
|
|
32
|
+
name: 'PageInfo',
|
|
33
|
+
fields: {
|
|
34
|
+
hasNextPage: { type: new GraphQLNonNull(GraphQLBoolean) },
|
|
35
|
+
hasPreviousPage: { type: new GraphQLNonNull(GraphQLBoolean) },
|
|
36
|
+
startCursor: { type: GraphQLString },
|
|
37
|
+
endCursor: { type: GraphQLString },
|
|
38
|
+
},
|
|
39
|
+
}));
|
|
40
|
+
const edge = (registry, model) => registry.object(`${model}Edge`, () => new GraphQLObjectType({
|
|
41
|
+
name: `${model}Edge`,
|
|
42
|
+
fields: () => ({
|
|
43
|
+
cursor: { type: new GraphQLNonNull(GraphQLString) },
|
|
44
|
+
node: { type: required(objectType(registry, model)) },
|
|
45
|
+
}),
|
|
46
|
+
}));
|
|
47
|
+
/**
|
|
48
|
+
* `PostConnection` at the root, `UserPostsConnection` on a relation, each
|
|
49
|
+
* with `totalCount` (D-107, D-134).
|
|
50
|
+
*/
|
|
51
|
+
export function connectionType(registry, name, model) {
|
|
52
|
+
return registry.object(name, () => new GraphQLObjectType({
|
|
53
|
+
name,
|
|
54
|
+
fields: () => ({
|
|
55
|
+
edges: {
|
|
56
|
+
type: new GraphQLNonNull(new GraphQLList(required(edge(registry, model)))),
|
|
57
|
+
},
|
|
58
|
+
pageInfo: { type: required(pageInfo(registry)) },
|
|
59
|
+
// A function the default resolver calls: counted when selected.
|
|
60
|
+
totalCount: { type: new GraphQLNonNull(GraphQLInt) },
|
|
61
|
+
}),
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* A relation's value in its row: `null` when absent and its read rule may
|
|
66
|
+
* hide it. Absent otherwise, the resolver that returned the row did not read
|
|
67
|
+
* it, such as one `extend` added.
|
|
68
|
+
*/
|
|
69
|
+
function readOf(source, model, field, ruled) {
|
|
70
|
+
const value = Reflect.get(Object(source), field);
|
|
71
|
+
if (value !== undefined)
|
|
72
|
+
return value;
|
|
73
|
+
if (ruled)
|
|
74
|
+
return null;
|
|
75
|
+
throw new Error(`${model}.${field} was not read; select it with context.select(info).`);
|
|
76
|
+
}
|
|
77
|
+
/** A `many` relation: a connection, or a list with `pagination: 'list'`. */
|
|
78
|
+
function manyField(registry, model, field, ruled) {
|
|
79
|
+
const target = objectType(registry, field.target);
|
|
80
|
+
const type = registry.settings.pagination === 'list'
|
|
81
|
+
? new GraphQLList(required(target))
|
|
82
|
+
: connectionType(registry, `${model}${upper(field.name)}Connection`, field.target);
|
|
83
|
+
return {
|
|
84
|
+
// Absent when the field's read rule hides it (D-44).
|
|
85
|
+
type: ruled ? type : new GraphQLNonNull(type),
|
|
86
|
+
args: listArgs(registry, field.target),
|
|
87
|
+
resolve: (source, args, context) => {
|
|
88
|
+
const value = readOf(source, model, field.name, ruled);
|
|
89
|
+
if (value === null)
|
|
90
|
+
return null;
|
|
91
|
+
const rows = rowsOf(value);
|
|
92
|
+
const { page } = listRead(registry, registry.entry(field.target), args, isMaster(context));
|
|
93
|
+
if (page === undefined)
|
|
94
|
+
return rows;
|
|
95
|
+
return {
|
|
96
|
+
...connection(rows, page, (row) => row),
|
|
97
|
+
// The row's `_count`, read when the connection selects it (D-134).
|
|
98
|
+
totalCount: () => {
|
|
99
|
+
const counts = Reflect.get(Object(source), '_count');
|
|
100
|
+
const count = Reflect.get(Object(counts), field.name);
|
|
101
|
+
if (typeof count === 'number')
|
|
102
|
+
return count;
|
|
103
|
+
throw new Error(`${model}.${field.name} was not counted; select it with context.select(info).`);
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/** `id`, the dates and each scalar field a caller other than Master reads. */
|
|
110
|
+
function scalarFields(registry, model) {
|
|
111
|
+
const entry = registry.entry(model);
|
|
112
|
+
const fields = {
|
|
113
|
+
id: { type: new GraphQLNonNull(GraphQLID) },
|
|
114
|
+
createdAt: { type: new GraphQLNonNull(DateTime) },
|
|
115
|
+
updatedAt: { type: new GraphQLNonNull(DateTime) },
|
|
116
|
+
};
|
|
117
|
+
for (const field of readable(entry)) {
|
|
118
|
+
if (field.kind !== 'scalar')
|
|
119
|
+
continue;
|
|
120
|
+
const ruled = entry.exposed.readRules[field.name] === 'rule';
|
|
121
|
+
const fieldType = entry.exposed.fieldTypes[field.name];
|
|
122
|
+
if (fieldType !== undefined) {
|
|
123
|
+
// A plugin field reads as its toJSON gives (D-122), typed by its
|
|
124
|
+
// shape (D-152).
|
|
125
|
+
const { type, nullable } = jsonOutput(registry, model, field);
|
|
126
|
+
fields[field.name] = {
|
|
127
|
+
type: field.optional || ruled || nullable ? type : new GraphQLNonNull(type),
|
|
128
|
+
resolve: (source) => {
|
|
129
|
+
const value = readOf(source, model, field.name, ruled);
|
|
130
|
+
return value === null ? null : fieldType.toJSON(value);
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const { type, nullable } = field.type === 'json'
|
|
136
|
+
? jsonOutput(registry, model, field)
|
|
137
|
+
: { type: valueType(registry, model, field), nullable: false };
|
|
138
|
+
fields[field.name] = {
|
|
139
|
+
type: field.optional || ruled || nullable ? type : new GraphQLNonNull(type),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return fields;
|
|
143
|
+
}
|
|
144
|
+
/** `Post`: a field only Master reads is absent, a ruled one nullable. */
|
|
145
|
+
export function objectType(registry, model) {
|
|
146
|
+
return registry.object(model, () => new GraphQLObjectType({
|
|
147
|
+
name: model,
|
|
148
|
+
description: registry.entry(model).exposed.description,
|
|
149
|
+
fields: () => {
|
|
150
|
+
const entry = registry.entry(model);
|
|
151
|
+
const fields = scalarFields(registry, model);
|
|
152
|
+
for (const field of readable(entry)) {
|
|
153
|
+
const ruled = entry.exposed.readRules[field.name] === 'rule';
|
|
154
|
+
if (field.kind === 'one') {
|
|
155
|
+
// Its target's read rule may hide the row (D-86).
|
|
156
|
+
fields[field.name] = {
|
|
157
|
+
type: objectType(registry, field.target),
|
|
158
|
+
resolve: (source) => readOf(source, model, field.name, ruled),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
else if (field.kind === 'many') {
|
|
162
|
+
fields[field.name] = manyField(registry, model, field, ruled);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
Object.assign(fields, registry.virtualFields(model));
|
|
166
|
+
return described(entry, fields);
|
|
167
|
+
},
|
|
168
|
+
}));
|
|
169
|
+
}
|
|
170
|
+
/** `DeletedPost`: a deleted row as it was, without its relations (D-102). */
|
|
171
|
+
export function deletedType(registry, model) {
|
|
172
|
+
const name = `Deleted${model}`;
|
|
173
|
+
return registry.object(name, () => new GraphQLObjectType({
|
|
174
|
+
name,
|
|
175
|
+
fields: () => described(registry.entry(model), scalarFields(registry, model)),
|
|
176
|
+
}));
|
|
177
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { DocumentNode } from 'graphql';
|
|
2
|
+
import { GraphQLError } from 'graphql';
|
|
3
|
+
/** The refusal of a document whose fields of one key make too many pairs. */
|
|
4
|
+
export declare function overlapRefusal(document: DocumentNode): GraphQLError | undefined;
|
|
5
|
+
//# sourceMappingURL=overlap.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"overlap.d.ts","sourceRoot":"","sources":["../src/overlap.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACX,YAAY,EAIZ,MAAM,SAAS,CAAA;AAChB,OAAO,EAAE,YAAY,EAAQ,MAAM,SAAS,CAAA;AAa5C,6EAA6E;AAC7E,wBAAgB,cAAc,CAC7B,QAAQ,EAAE,YAAY,GACpB,YAAY,GAAG,SAAS,CAsE1B"}
|
package/dist/overlap.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { GraphQLError, Kind } from 'graphql';
|
|
2
|
+
/** At about 28 µs a pair, the rule's slowest, about 30 ms of validation. */
|
|
3
|
+
const maxPairs = 1000;
|
|
4
|
+
const pairsOf = (fields) => (fields * (fields - 1)) / 2;
|
|
5
|
+
/** The refusal of a document whose fields of one key make too many pairs. */
|
|
6
|
+
export function overlapRefusal(document) {
|
|
7
|
+
const fragments = new Map();
|
|
8
|
+
for (const definition of document.definitions) {
|
|
9
|
+
if (definition.kind === Kind.FRAGMENT_DEFINITION) {
|
|
10
|
+
fragments.set(definition.name.value, definition);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
// A selection set merged with no other is counted once, wherever it is
|
|
14
|
+
// reached, as the rule compares its fields once.
|
|
15
|
+
const counted = new Set();
|
|
16
|
+
let pairs = 0;
|
|
17
|
+
/**
|
|
18
|
+
* The fields of `sets` by response key, fragments spread once, each with
|
|
19
|
+
* how many came from each fragment spread in `sets`, or from `sets`.
|
|
20
|
+
*/
|
|
21
|
+
const merged = (sets) => {
|
|
22
|
+
const byKey = new Map();
|
|
23
|
+
const spread = new Set();
|
|
24
|
+
const collect = (set, from) => {
|
|
25
|
+
for (const selection of set.selections) {
|
|
26
|
+
if (selection.kind === Kind.FIELD) {
|
|
27
|
+
const key = selection.alias?.value ?? selection.name.value;
|
|
28
|
+
const same = byKey.get(key) ?? { fields: [], from: new Map() };
|
|
29
|
+
byKey.set(key, same);
|
|
30
|
+
same.fields.push(selection);
|
|
31
|
+
same.from.set(from, (same.from.get(from) ?? 0) + 1);
|
|
32
|
+
}
|
|
33
|
+
else if (selection.kind === Kind.INLINE_FRAGMENT) {
|
|
34
|
+
collect(selection.selectionSet, from);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
const name = selection.name.value;
|
|
38
|
+
const fragment = fragments.get(name);
|
|
39
|
+
if (fragment === undefined || spread.has(name))
|
|
40
|
+
continue;
|
|
41
|
+
spread.add(name);
|
|
42
|
+
collect(fragment.selectionSet, from === '' ? name : from);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
for (const set of sets)
|
|
47
|
+
collect(set, '');
|
|
48
|
+
return byKey.values();
|
|
49
|
+
};
|
|
50
|
+
const walk = (sets) => {
|
|
51
|
+
const [only] = sets;
|
|
52
|
+
if (sets.length === 1 && only !== undefined) {
|
|
53
|
+
if (counted.has(only))
|
|
54
|
+
return;
|
|
55
|
+
counted.add(only);
|
|
56
|
+
}
|
|
57
|
+
for (const { fields, from } of merged(sets)) {
|
|
58
|
+
if (pairs > maxPairs)
|
|
59
|
+
return;
|
|
60
|
+
pairs += pairsOf(fields.length);
|
|
61
|
+
// The pairs within a fragment are counted where it is defined.
|
|
62
|
+
for (const [fragment, count] of from) {
|
|
63
|
+
if (fragment !== '')
|
|
64
|
+
pairs -= pairsOf(count);
|
|
65
|
+
}
|
|
66
|
+
if (from.size === 1 && !from.has(''))
|
|
67
|
+
continue;
|
|
68
|
+
const below = fields.flatMap((field) => field.selectionSet ?? []);
|
|
69
|
+
if (below.length > 0)
|
|
70
|
+
walk(below);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
for (const definition of document.definitions) {
|
|
74
|
+
if (definition.kind === Kind.OPERATION_DEFINITION ||
|
|
75
|
+
definition.kind === Kind.FRAGMENT_DEFINITION) {
|
|
76
|
+
walk([definition.selectionSet]);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (pairs <= maxPairs)
|
|
80
|
+
return undefined;
|
|
81
|
+
return new GraphQLError('The document repeats fields under one name too often. Ask for each field once.');
|
|
82
|
+
}
|
package/dist/pages.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Page } from '@ultimabase/http/internal';
|
|
2
|
+
import type { Args, Registry } from './registry.ts';
|
|
3
|
+
import type { Entry } from './registry.ts';
|
|
4
|
+
/** The kernel's `where` of a list or count's arguments. */
|
|
5
|
+
export declare function whereOf(registry: Registry, model: string, args: Args): Args;
|
|
6
|
+
/** A list's kernel read, and its Relay page unless `pagination: 'list'`. */
|
|
7
|
+
export declare function listRead(registry: Registry, entry: Entry, args: Args, master: boolean): {
|
|
8
|
+
readonly read: Args;
|
|
9
|
+
readonly page: Page['relay'];
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=pages.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pages.d.ts","sourceRoot":"","sources":["../src/pages.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,2BAA2B,CAAA;AAGrD,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACnD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAA;AAE1C,2DAA2D;AAC3D,wBAAgB,OAAO,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG,IAAI,CAK3E;AAED,4EAA4E;AAC5E,wBAAgB,QAAQ,CACvB,QAAQ,EAAE,QAAQ,EAClB,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,IAAI,EACV,MAAM,EAAE,OAAO,GACb;IAAE,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,CAAA;CAAE,CAgBvD"}
|
package/dist/pages.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { pageOf } from '@ultimabase/http/internal';
|
|
2
|
+
import { plainWhere } from './json-fields.js';
|
|
3
|
+
/** The kernel's `where` of a list or count's arguments. */
|
|
4
|
+
export function whereOf(registry, model, args) {
|
|
5
|
+
const { where } = args;
|
|
6
|
+
return typeof where === 'object' && where !== null
|
|
7
|
+
? { where: plainWhere(registry, model, { ...where }) }
|
|
8
|
+
: {};
|
|
9
|
+
}
|
|
10
|
+
/** A list's kernel read, and its Relay page unless `pagination: 'list'`. */
|
|
11
|
+
export function listRead(registry, entry, args, master) {
|
|
12
|
+
const { orderBy } = args;
|
|
13
|
+
const { read, relay } = pageOf(registry.settings.pagination, entry, args, master);
|
|
14
|
+
return {
|
|
15
|
+
read: {
|
|
16
|
+
...whereOf(registry, entry.model.name, args),
|
|
17
|
+
...(orderBy === undefined || orderBy === null ? {} : { orderBy }),
|
|
18
|
+
...read,
|
|
19
|
+
},
|
|
20
|
+
page: relay,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { GraphQLFieldConfigMap, GraphQLInputObjectType, GraphQLObjectType, GraphQLResolveInfo, GraphQLScalarType } from 'graphql';
|
|
2
|
+
import { GraphQLEnumType, GraphQLList, GraphQLNonNull } from 'graphql';
|
|
3
|
+
import type { Caller, Client, ExposedModel, SchemaField, SchemaModel } from 'ultimabase';
|
|
4
|
+
import { DataTypes } from './data-types.ts';
|
|
5
|
+
/** A model's key of `.with` (D-107). */
|
|
6
|
+
export type GraphqlWith = {
|
|
7
|
+
readonly plural?: string;
|
|
8
|
+
readonly maxTake?: number;
|
|
9
|
+
} | false;
|
|
10
|
+
/** What every resolver receives, the generated ones and those `extend` adds. */
|
|
11
|
+
export type Context = {
|
|
12
|
+
readonly caller: Caller;
|
|
13
|
+
/** The request's client, which a function's `run` receives (D-113). */
|
|
14
|
+
readonly client: Client;
|
|
15
|
+
/**
|
|
16
|
+
* The kernel's `select` for what the client asks of the rows a field
|
|
17
|
+
* returns, relations and their pages included; spread it to add a field.
|
|
18
|
+
* Its type names `id` only: the rest depends on the document.
|
|
19
|
+
*/
|
|
20
|
+
readonly select: (info: GraphQLResolveInfo) => {
|
|
21
|
+
readonly id: true;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
/** A kernel query's options: run as the request's caller (D-106). */
|
|
25
|
+
export declare const as: (context: Context) => {
|
|
26
|
+
readonly as: Caller;
|
|
27
|
+
};
|
|
28
|
+
export type Args = {
|
|
29
|
+
readonly [arg: string]: unknown;
|
|
30
|
+
};
|
|
31
|
+
export type Scalar = Extract<SchemaField, {
|
|
32
|
+
readonly kind: 'scalar';
|
|
33
|
+
}>;
|
|
34
|
+
export type Entry = {
|
|
35
|
+
readonly exposed: ExposedModel<GraphqlWith>;
|
|
36
|
+
readonly model: SchemaModel;
|
|
37
|
+
/** In the root fields; a hidden model is reached by relations only. */
|
|
38
|
+
readonly visible: boolean;
|
|
39
|
+
readonly single: string;
|
|
40
|
+
readonly list: string;
|
|
41
|
+
/** The rows a list returns without `first`, `last` or `take`. */
|
|
42
|
+
readonly take: number;
|
|
43
|
+
readonly maxTake: number;
|
|
44
|
+
};
|
|
45
|
+
export type Settings = {
|
|
46
|
+
readonly pagination: 'connection' | 'list';
|
|
47
|
+
};
|
|
48
|
+
export declare class Registry {
|
|
49
|
+
#private;
|
|
50
|
+
readonly entries: ReadonlyMap<string, Entry>;
|
|
51
|
+
readonly settings: Settings;
|
|
52
|
+
/** The types functions and json fields give, shared by both (D-125). */
|
|
53
|
+
readonly data: DataTypes;
|
|
54
|
+
/** A model's virtual fields, which the functions' outputs build (D-151). */
|
|
55
|
+
virtualFields: (model: string) => GraphQLFieldConfigMap<unknown, Context>;
|
|
56
|
+
constructor(entries: ReadonlyMap<string, Entry>, settings: Settings);
|
|
57
|
+
entry(model: string): Entry;
|
|
58
|
+
object(name: string, build: () => GraphQLObjectType): GraphQLObjectType;
|
|
59
|
+
input(name: string, build: () => GraphQLInputObjectType): GraphQLInputObjectType;
|
|
60
|
+
enum(name: string, build: () => GraphQLEnumType): GraphQLEnumType;
|
|
61
|
+
}
|
|
62
|
+
/** Each field's `description` (D-150), on the fields of a type or an input. */
|
|
63
|
+
export declare function described<C extends {
|
|
64
|
+
readonly description?: string | null | undefined;
|
|
65
|
+
}>(entry: Entry, fields: {
|
|
66
|
+
[field: string]: C;
|
|
67
|
+
}): {
|
|
68
|
+
[field: string]: C;
|
|
69
|
+
};
|
|
70
|
+
export declare const scalarTypes: {
|
|
71
|
+
readonly [T in Exclude<Scalar['type'], 'enum'>]: GraphQLScalarType;
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* A scalar field's type; an enum's is `PostStatus`, built once, and a
|
|
75
|
+
* `defineEnum`'s its own name, one type for every field (D-150).
|
|
76
|
+
*/
|
|
77
|
+
export declare function scalarType(registry: Registry, model: string, field: Scalar): GraphQLScalarType | GraphQLEnumType;
|
|
78
|
+
/** A scalar field's value, read or written: a list's is `[Float!]` (D-127). */
|
|
79
|
+
export declare const valueType: (registry: Registry, model: string, field: Scalar) => GraphQLScalarType | GraphQLEnumType | GraphQLList<GraphQLNonNull<GraphQLScalarType | GraphQLEnumType>>;
|
|
80
|
+
//# sourceMappingURL=registry.d.ts.map
|