@ultimabase/rest 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/entries.d.ts +38 -0
- package/dist/entries.d.ts.map +1 -0
- package/dist/entries.js +47 -0
- package/dist/fields.d.ts +77 -0
- package/dist/fields.d.ts.map +1 -0
- package/dist/fields.js +288 -0
- package/dist/functions.d.ts +28 -0
- package/dist/functions.d.ts.map +1 -0
- package/dist/functions.js +156 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +62 -0
- package/dist/json-schema.d.ts +8 -0
- package/dist/json-schema.d.ts.map +1 -0
- package/dist/json-schema.js +44 -0
- package/dist/limits.d.ts +6 -0
- package/dist/limits.d.ts.map +1 -0
- package/dist/limits.js +17 -0
- package/dist/live.d.ts +15 -0
- package/dist/live.d.ts.map +1 -0
- package/dist/live.js +168 -0
- package/dist/models.d.ts +8 -0
- package/dist/models.d.ts.map +1 -0
- package/dist/models.js +136 -0
- package/dist/openapi-functions.d.ts +11 -0
- package/dist/openapi-functions.d.ts.map +1 -0
- package/dist/openapi-functions.js +85 -0
- package/dist/openapi-inputs.d.ts +30 -0
- package/dist/openapi-inputs.d.ts.map +1 -0
- package/dist/openapi-inputs.js +316 -0
- package/dist/openapi-models.d.ts +14 -0
- package/dist/openapi-models.d.ts.map +1 -0
- package/dist/openapi-models.js +280 -0
- package/dist/openapi.d.ts +47 -0
- package/dist/openapi.d.ts.map +1 -0
- package/dist/openapi.js +150 -0
- package/dist/params.d.ts +11 -0
- package/dist/params.d.ts.map +1 -0
- package/dist/params.js +27 -0
- package/dist/sse.d.ts +17 -0
- package/dist/sse.d.ts.map +1 -0
- package/dist/sse.js +63 -0
- package/dist/streams.d.ts +73 -0
- package/dist/streams.d.ts.map +1 -0
- package/dist/streams.js +218 -0
- package/dist/values.d.ts +7 -0
- package/dist/values.d.ts.map +1 -0
- package/dist/values.js +123 -0
- package/package.json +47 -3
- package/src/entries.ts +82 -0
- package/src/fields.ts +391 -0
- package/src/functions.ts +205 -0
- package/src/index.ts +105 -0
- package/src/json-schema.ts +51 -0
- package/src/limits.ts +28 -0
- package/src/live.ts +223 -0
- package/src/models.ts +180 -0
- package/src/openapi-functions.ts +127 -0
- package/src/openapi-inputs.ts +399 -0
- package/src/openapi-models.ts +367 -0
- package/src/openapi.ts +204 -0
- package/src/params.ts +39 -0
- package/src/sse.ts +79 -0
- package/src/streams.ts +301 -0
- package/src/values.ts +163 -0
package/dist/models.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { validate } from '@ultimabase/http';
|
|
2
|
+
import { badInput, connection, pageArgs, run } from '@ultimabase/http/internal';
|
|
3
|
+
import { InvalidQuery, PublicError } from 'ultimabase';
|
|
4
|
+
import { defaultShape, filterOf, readerOf, shaped } from './fields.js';
|
|
5
|
+
import { bounded } from './limits.js';
|
|
6
|
+
import { liveList, liveRow, wantsEvents } from './live.js';
|
|
7
|
+
import { paramsOf } from './params.js';
|
|
8
|
+
import { dataOf, isArgs, whereOf } from './values.js';
|
|
9
|
+
/** Any JSON body: the kernel checks the data (D-129). */
|
|
10
|
+
export const anyBody = {
|
|
11
|
+
'~standard': { validate: (value) => ({ value }) },
|
|
12
|
+
};
|
|
13
|
+
const fieldParams = { json: ['select', 'include'], text: [] };
|
|
14
|
+
/** The kernel read of one row's `select` or `include`. */
|
|
15
|
+
function oneRead(api, entry, caller, url) {
|
|
16
|
+
const params = paramsOf(url, fieldParams);
|
|
17
|
+
const read = readerOf(api, caller.kind === 'master').fields(entry, params, '');
|
|
18
|
+
bounded(api, caller, read.shape, 1);
|
|
19
|
+
return read;
|
|
20
|
+
}
|
|
21
|
+
/** `GET /api/posts`: a connection, or a list with `pagination: 'list'`. */
|
|
22
|
+
async function list(api, entry, caller, url) {
|
|
23
|
+
const counts = api.pagination === 'connection';
|
|
24
|
+
const params = paramsOf(url, {
|
|
25
|
+
json: [
|
|
26
|
+
'where',
|
|
27
|
+
'orderBy',
|
|
28
|
+
'select',
|
|
29
|
+
'include',
|
|
30
|
+
...pageArgs(api.pagination).filter((arg) => arg !== 'after' && arg !== 'before'),
|
|
31
|
+
...(counts ? ['totalCount'] : []),
|
|
32
|
+
],
|
|
33
|
+
text: counts ? ['after', 'before'] : [],
|
|
34
|
+
});
|
|
35
|
+
const { totalCount, ...args } = params;
|
|
36
|
+
if (totalCount !== undefined && typeof totalCount !== 'boolean') {
|
|
37
|
+
throw badInput('totalCount must be true or false.');
|
|
38
|
+
}
|
|
39
|
+
const read = readerOf(api, caller.kind === 'master').list(entry, args, '');
|
|
40
|
+
// A count reads the rows it counts, however few the page returns.
|
|
41
|
+
const size = totalCount === true ? Math.max(read.size, entry.take) : read.size;
|
|
42
|
+
bounded(api, caller, read.shape, size);
|
|
43
|
+
const { queries } = entry.exposed;
|
|
44
|
+
const rows = await queries.findMany(read.read, { as: caller });
|
|
45
|
+
const node = (row) => shaped(row, read.shape);
|
|
46
|
+
if (read.relay === undefined)
|
|
47
|
+
return rows.map(node);
|
|
48
|
+
const page = connection(rows, read.relay, node);
|
|
49
|
+
if (totalCount !== true)
|
|
50
|
+
return page;
|
|
51
|
+
const where = filterOf(api, entry, args['where'], 'where');
|
|
52
|
+
return { ...page, totalCount: await queries.count(where, { as: caller }) };
|
|
53
|
+
}
|
|
54
|
+
const notFound = (entry) => new PublicError('NOT_FOUND', `No ${entry.model.name} matches.`);
|
|
55
|
+
const written = (api, entry, body) => isArgs(body) ? dataOf(api, entry.model.name, body, 'data') : body;
|
|
56
|
+
const upsertKeys = new Set(['where', 'create', 'update']);
|
|
57
|
+
/** An upsert's body, its dates read; the kernel checks the rest (D-142). */
|
|
58
|
+
function upserting(api, entry, body) {
|
|
59
|
+
const { name } = entry.model;
|
|
60
|
+
if (!isArgs(body))
|
|
61
|
+
throw new InvalidQuery('body', 'The body must be an object.');
|
|
62
|
+
const read = {};
|
|
63
|
+
for (const [key, value] of Object.entries(body)) {
|
|
64
|
+
if (!upsertKeys.has(key)) {
|
|
65
|
+
throw new InvalidQuery(key, `Unknown key '${key}' on ${name}.`);
|
|
66
|
+
}
|
|
67
|
+
if (!isArgs(value))
|
|
68
|
+
read[key] = value;
|
|
69
|
+
else if (key === 'where')
|
|
70
|
+
read[key] = whereOf(api, name, value, key);
|
|
71
|
+
else
|
|
72
|
+
read[key] = dataOf(api, name, value, key);
|
|
73
|
+
}
|
|
74
|
+
return read;
|
|
75
|
+
}
|
|
76
|
+
/** Each route of a visible model, under `base`; its reads live with `live`. */
|
|
77
|
+
export function modelRoutes(app, api, entry, base, live) {
|
|
78
|
+
const path = `${base}/${entry.plural}`;
|
|
79
|
+
const { queries } = entry.exposed;
|
|
80
|
+
app.get(path, async (c) => {
|
|
81
|
+
const { caller } = c.var;
|
|
82
|
+
if (wantsEvents(c.req.raw)) {
|
|
83
|
+
return liveList(api, live, entry, caller, c.req.raw);
|
|
84
|
+
}
|
|
85
|
+
return c.json(await run(() => list(api, entry, caller, c.req.url)));
|
|
86
|
+
});
|
|
87
|
+
/** The row `query` returns, shaped as `url` asks. */
|
|
88
|
+
const one = async (caller, url, query) => {
|
|
89
|
+
const { row, shape } = await run(async () => {
|
|
90
|
+
const fields = oneRead(api, entry, caller, url);
|
|
91
|
+
return { row: await query(fields.read, { as: caller }), ...fields };
|
|
92
|
+
});
|
|
93
|
+
if (row === null)
|
|
94
|
+
throw notFound(entry);
|
|
95
|
+
return shaped(row, shape);
|
|
96
|
+
};
|
|
97
|
+
app.get(`${path}/:id`, async (c) => {
|
|
98
|
+
const { caller } = c.var;
|
|
99
|
+
const id = c.req.param('id');
|
|
100
|
+
if (wantsEvents(c.req.raw)) {
|
|
101
|
+
return liveRow(api, live, entry, caller, c.req.raw, id);
|
|
102
|
+
}
|
|
103
|
+
return c.json(await one(caller, c.req.url, async (read, as) => queries.findUnique({ ...read, where: { id } }, as)));
|
|
104
|
+
});
|
|
105
|
+
app.post(path, validate('json', anyBody), async (c) => {
|
|
106
|
+
const body = c.req.valid('json');
|
|
107
|
+
const row = await one(c.var.caller, c.req.url, async (read, as) => queries.create({ ...read, data: written(api, entry, body) }, as));
|
|
108
|
+
return c.json(row, 201);
|
|
109
|
+
});
|
|
110
|
+
// 201 when it created the row, 200 when it updated it (D-142).
|
|
111
|
+
app.put(path, validate('json', anyBody), async (c) => {
|
|
112
|
+
const body = c.req.valid('json');
|
|
113
|
+
const { caller } = c.var;
|
|
114
|
+
const { row, created, shape } = await run(async () => {
|
|
115
|
+
const fields = oneRead(api, entry, caller, c.req.url);
|
|
116
|
+
const args = { ...fields.read, ...upserting(api, entry, body) };
|
|
117
|
+
const upserted = await entry.exposed.upsert(args, { as: caller });
|
|
118
|
+
return { ...upserted, shape: fields.shape };
|
|
119
|
+
});
|
|
120
|
+
return c.json(shaped(row, shape), created ? 201 : 200);
|
|
121
|
+
});
|
|
122
|
+
app.patch(`${path}/:id`, validate('json', anyBody), async (c) => {
|
|
123
|
+
const body = c.req.valid('json');
|
|
124
|
+
const where = { id: c.req.param('id') };
|
|
125
|
+
return c.json(await one(c.var.caller, c.req.url, async (read, as) => queries.update({ ...read, where, data: written(api, entry, body) }, as)));
|
|
126
|
+
});
|
|
127
|
+
app.delete(`${path}/:id`, async (c) => {
|
|
128
|
+
const { caller } = c.var;
|
|
129
|
+
const row = await run(async () => {
|
|
130
|
+
paramsOf(c.req.url, { json: [], text: [] });
|
|
131
|
+
return queries.delete({ where: { id: c.req.param('id') } }, { as: caller });
|
|
132
|
+
});
|
|
133
|
+
// As it was, without its relations (D-102).
|
|
134
|
+
return c.json(shaped(row, defaultShape(entry)));
|
|
135
|
+
});
|
|
136
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ExposedOutput } from 'ultimabase';
|
|
2
|
+
import type { Routed } from './functions.ts';
|
|
3
|
+
import type { Doc } from './openapi-inputs.ts';
|
|
4
|
+
import type { Schema } from './openapi.ts';
|
|
5
|
+
/** A model, a `defineType` or a schema, held as `form` says (D-109). */
|
|
6
|
+
export declare function outputOf(doc: Doc, output: ExposedOutput, owner: string, root: string): Schema;
|
|
7
|
+
/** `/functions/{name}`, a query over GET, a mutation over POST (D-109). */
|
|
8
|
+
export declare function functionPaths(doc: Doc, functions: readonly Routed[]): {
|
|
9
|
+
[path: string]: Schema;
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=openapi-functions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openapi-functions.d.ts","sourceRoot":"","sources":["../src/openapi-functions.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAe,MAAM,YAAY,CAAA;AAC5D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAE5C,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,qBAAqB,CAAA;AAE9C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AAyC1C,wEAAwE;AACxE,wBAAgB,QAAQ,CACvB,GAAG,EAAE,GAAG,EACR,MAAM,EAAE,aAAa,EACrB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,GACV,MAAM,CAWR;AAsBD,2EAA2E;AAC3E,wBAAgB,aAAa,CAC5B,GAAG,EAAE,GAAG,EACR,SAAS,EAAE,SAAS,MAAM,EAAE,GAC1B;IAAE,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,CAiC5B"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { isNode, jsonSchemaOf } from '@ultimabase/http/internal';
|
|
2
|
+
import { getParams } from './functions.js';
|
|
3
|
+
import { rowOf } from './openapi-models.js';
|
|
4
|
+
import { arrayOf, describe, failed, hoisted, json, objectOf, orNull, } from './openapi.js';
|
|
5
|
+
const upper = (name) => name.charAt(0).toUpperCase() + name.slice(1);
|
|
6
|
+
const prefix = '#/components/schemas/';
|
|
7
|
+
/** A schema, or the component it refers to. */
|
|
8
|
+
function resolve(doc, schema) {
|
|
9
|
+
const ref = schema['$ref'];
|
|
10
|
+
if (typeof ref !== 'string' || !ref.startsWith(prefix))
|
|
11
|
+
return schema;
|
|
12
|
+
return doc.components.schemas[ref.slice(prefix.length)] ?? {};
|
|
13
|
+
}
|
|
14
|
+
/** A `defineType`, each of its fields always given (D-109). */
|
|
15
|
+
function typeOf(doc, type, owner) {
|
|
16
|
+
return doc.components.ref(type.name, () => describe(objectOf(Object.fromEntries([...type.fields].map(([name, field]) => [
|
|
17
|
+
name,
|
|
18
|
+
outputOf(doc, field, owner, `${type.name}${upper(name)}`),
|
|
19
|
+
])), [...type.fields.keys()]), type.description));
|
|
20
|
+
}
|
|
21
|
+
/** A model, a `defineType` or a schema, held as `form` says (D-109). */
|
|
22
|
+
export function outputOf(doc, output, owner, root) {
|
|
23
|
+
if (output.kind === 'schema') {
|
|
24
|
+
const node = jsonSchemaOf(output.schema, 'output', `${owner}: output`);
|
|
25
|
+
return hoisted(doc.components, node, 'output', owner, root);
|
|
26
|
+
}
|
|
27
|
+
const one = output.kind === 'model'
|
|
28
|
+
? rowOf(doc, output.name)
|
|
29
|
+
: typeOf(doc, output.type, owner);
|
|
30
|
+
if (output.form === 'list')
|
|
31
|
+
return arrayOf(one);
|
|
32
|
+
return output.form === 'nullable' ? orNull(one) : one;
|
|
33
|
+
}
|
|
34
|
+
/** A GET's parameters, as `getParams` reads them. */
|
|
35
|
+
function paramsOf(doc, input, schema) {
|
|
36
|
+
const { accepted, flat } = getParams(input);
|
|
37
|
+
if (!flat) {
|
|
38
|
+
const as = accepted.text.length > 0 ? { schema } : { content: json(schema) };
|
|
39
|
+
return [{ name: 'input', in: 'query', required: true, ...as }];
|
|
40
|
+
}
|
|
41
|
+
const object = resolve(doc, schema);
|
|
42
|
+
const properties = isNode(object['properties']) ? object['properties'] : {};
|
|
43
|
+
const required = Array.isArray(object['required']) ? object['required'] : [];
|
|
44
|
+
return Object.entries(properties).map(([name, property]) => ({
|
|
45
|
+
name,
|
|
46
|
+
in: 'query',
|
|
47
|
+
...(required.includes(name) && { required: true }),
|
|
48
|
+
...(accepted.text.includes(name)
|
|
49
|
+
? { schema: property }
|
|
50
|
+
: { content: json(isNode(property) ? property : {}) }),
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
/** `/functions/{name}`, a query over GET, a mutation over POST (D-109). */
|
|
54
|
+
export function functionPaths(doc, functions) {
|
|
55
|
+
const paths = {};
|
|
56
|
+
for (const { fn, owner, name, input } of functions) {
|
|
57
|
+
const base = upper(name);
|
|
58
|
+
const given = input === undefined
|
|
59
|
+
? undefined
|
|
60
|
+
: hoisted(doc.components, input, 'input', owner, `${base}Input`);
|
|
61
|
+
const output = fn.output === undefined
|
|
62
|
+
? { const: true }
|
|
63
|
+
: outputOf(doc, fn.output, owner, `${base}Output`);
|
|
64
|
+
const operation = {
|
|
65
|
+
operationId: doc.components.operation(name, owner, 'name'),
|
|
66
|
+
tags: ['functions'],
|
|
67
|
+
...(fn.description !== undefined && { description: fn.description }),
|
|
68
|
+
...(fn.query && given !== undefined && input !== undefined
|
|
69
|
+
? { parameters: paramsOf(doc, input, given) }
|
|
70
|
+
: {}),
|
|
71
|
+
...(!fn.query &&
|
|
72
|
+
given !== undefined && {
|
|
73
|
+
requestBody: { required: true, content: json(given) },
|
|
74
|
+
}),
|
|
75
|
+
responses: {
|
|
76
|
+
200: { description: `What ${name} returns.`, content: json(output) },
|
|
77
|
+
...failed,
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
paths[`/functions/${name}`] = fn.query
|
|
81
|
+
? { get: operation }
|
|
82
|
+
: { post: operation };
|
|
83
|
+
}
|
|
84
|
+
return paths;
|
|
85
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { SchemaField } from 'ultimabase';
|
|
2
|
+
import type { Api, Entry } from './entries.ts';
|
|
3
|
+
import type { Components, Schema } from './openapi.ts';
|
|
4
|
+
type Scalar = Extract<SchemaField, {
|
|
5
|
+
readonly kind: 'scalar';
|
|
6
|
+
}>;
|
|
7
|
+
/** What the document is built from: its components and REST's models. */
|
|
8
|
+
export type Doc = {
|
|
9
|
+
readonly components: Components;
|
|
10
|
+
readonly api: Api;
|
|
11
|
+
};
|
|
12
|
+
/** A field's value, a plugin field's as its shape says (D-122, D-152). */
|
|
13
|
+
export declare function valueOf(doc: Doc, entry: Entry, field: Scalar, side: 'input' | 'output'): Schema;
|
|
14
|
+
/** `PostWhereInput`, its relation filters included. */
|
|
15
|
+
export declare function whereOf(doc: Doc, model: string): Schema;
|
|
16
|
+
/** `PostOrderByInput`, an object or a list of them (D-94, D-134). */
|
|
17
|
+
export declare function orderByOf(doc: Doc, model: string): Schema;
|
|
18
|
+
/**
|
|
19
|
+
* `PostWhereUniqueInput`, as REST takes it: the fields of one unique set,
|
|
20
|
+
* `{ id }` or `{ author, slug }`, a relation by its target's id.
|
|
21
|
+
*/
|
|
22
|
+
export declare function uniqueOf(doc: Doc, model: string): Schema;
|
|
23
|
+
/**
|
|
24
|
+
* `PostCreateInput` and `PostUpdateInput`: every field, those only Master
|
|
25
|
+
* reads included; a create needs the fields `create` needs (D-53). Through
|
|
26
|
+
* a relation, `CommentCreateWithoutPostInput`, without it (D-138).
|
|
27
|
+
*/
|
|
28
|
+
export declare function dataOf(doc: Doc, model: string, creating: boolean, without?: string): Schema;
|
|
29
|
+
export {};
|
|
30
|
+
//# sourceMappingURL=openapi-inputs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openapi-inputs.d.ts","sourceRoot":"","sources":["../src/openapi-inputs.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAG7C,OAAO,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,cAAc,CAAA;AAE9C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AAWtD,KAAK,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAC,CAAA;AAG/D,yEAAyE;AACzE,MAAM,MAAM,GAAG,GAAG;IAAE,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAA;CAAE,CAAA;AAqFxE,0EAA0E;AAC1E,wBAAgB,OAAO,CACtB,GAAG,EAAE,GAAG,EACR,KAAK,EAAE,KAAK,EACZ,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,OAAO,GAAG,QAAQ,GACtB,MAAM,CAOR;AAqFD,uDAAuD;AACvD,wBAAgB,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAwBvD;AAYD,qEAAqE;AACrE,wBAAgB,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CA+BzD;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAqBxD;AA4DD;;;;GAIG;AACH,wBAAgB,MAAM,CACrB,GAAG,EAAE,GAAG,EACR,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,OAAO,EACjB,OAAO,CAAC,EAAE,MAAM,GACd,MAAM,CAyBR"}
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
// The inputs of the document (D-144), named and typed as GraphQL's (D-129):
|
|
2
|
+
// each field's value, the filters, `where`, `orderBy`, the unique target
|
|
3
|
+
// in REST's flat form, and the data a write takes.
|
|
4
|
+
import { jsonSchemaOf, readable } from '@ultimabase/http/internal';
|
|
5
|
+
import { Refused } from 'ultimabase';
|
|
6
|
+
import { decimalPattern } from 'ultimabase/internal';
|
|
7
|
+
import { entryOf } from './entries.js';
|
|
8
|
+
import { arrayOf, describe, described, hoisted, objectOf, orNull, refTo, } from './openapi.js';
|
|
9
|
+
const upper = (name) => name.charAt(0).toUpperCase() + name.slice(1);
|
|
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 id = { type: 'string' };
|
|
13
|
+
const dateTime = { type: 'string', format: 'date-time' };
|
|
14
|
+
/** One value of `field`, a list's item, by GraphQL's type name. */
|
|
15
|
+
function item(doc, model, field, side) {
|
|
16
|
+
const named = `${model}${upper(field.name)}`;
|
|
17
|
+
switch (field.type) {
|
|
18
|
+
case 'string':
|
|
19
|
+
return { name: 'String', schema: { type: 'string' } };
|
|
20
|
+
case 'number':
|
|
21
|
+
return field.integer === true
|
|
22
|
+
? { name: 'Int', schema: { type: 'integer', format: 'int32' } }
|
|
23
|
+
: { name: 'Float', schema: { type: 'number' } };
|
|
24
|
+
case 'decimal':
|
|
25
|
+
return {
|
|
26
|
+
name: 'Decimal',
|
|
27
|
+
schema: { type: 'string', pattern: decimalPattern },
|
|
28
|
+
};
|
|
29
|
+
case 'boolean':
|
|
30
|
+
return { name: 'Boolean', schema: { type: 'boolean' } };
|
|
31
|
+
case 'dateTime':
|
|
32
|
+
return { name: 'DateTime', schema: dateTime };
|
|
33
|
+
case 'date':
|
|
34
|
+
return { name: 'Date', schema: { type: 'string', format: 'date' } };
|
|
35
|
+
case 'enum': {
|
|
36
|
+
const values = field.values ?? [];
|
|
37
|
+
const shared = entryOf(doc.api, model).exposed.enums[field.name];
|
|
38
|
+
if (shared !== undefined) {
|
|
39
|
+
const { name, description } = shared;
|
|
40
|
+
const schema = describe({ type: 'string', enum: values }, description);
|
|
41
|
+
const owner = `${model}.${field.name}`;
|
|
42
|
+
return { name, schema: doc.components.shared(name, schema, owner) };
|
|
43
|
+
}
|
|
44
|
+
const schema = doc.components.ref(named, () => ({
|
|
45
|
+
type: 'string',
|
|
46
|
+
enum: values,
|
|
47
|
+
}));
|
|
48
|
+
return { name: named, schema };
|
|
49
|
+
}
|
|
50
|
+
case 'json':
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
return { name: 'JSON', schema: jsonOf(doc, model, field, side) };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* A json field's value: its validator's JSON Schema, any JSON without
|
|
57
|
+
* (D-125); a plugin field's read value, its shape's (D-152).
|
|
58
|
+
*/
|
|
59
|
+
function jsonOf(doc, model, field, side) {
|
|
60
|
+
const { validators, fieldTypes } = entryOf(doc.api, model).exposed;
|
|
61
|
+
const validator = validators[field.name] ?? fieldTypes[field.name]?.shape;
|
|
62
|
+
if (validator === undefined)
|
|
63
|
+
return {};
|
|
64
|
+
const owner = `${model}.${field.name}`;
|
|
65
|
+
let node;
|
|
66
|
+
try {
|
|
67
|
+
node = jsonSchemaOf(validator, side, owner);
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
// As GraphQL types it: `JSON` without an export.
|
|
71
|
+
if (error instanceof Refused)
|
|
72
|
+
return {};
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
const root = `${model}${upper(field.name)}${side === 'input' ? 'Input' : ''}`;
|
|
76
|
+
return hoisted(doc.components, node, side, owner, root);
|
|
77
|
+
}
|
|
78
|
+
/** A field's value, a plugin field's as its shape says (D-122, D-152). */
|
|
79
|
+
export function valueOf(doc, entry, field, side) {
|
|
80
|
+
if (entry.exposed.fieldTypes[field.name] !== undefined) {
|
|
81
|
+
// What it takes is the field type's own.
|
|
82
|
+
return side === 'input' ? {} : jsonOf(doc, entry.model.name, field, side);
|
|
83
|
+
}
|
|
84
|
+
const { schema } = item(doc, entry.model.name, field, side);
|
|
85
|
+
return field.list ? arrayOf(schema) : schema;
|
|
86
|
+
}
|
|
87
|
+
const base = ['equals', 'not', 'in', 'notIn'];
|
|
88
|
+
const ordered = [...base, 'lt', 'lte', 'gt', 'gte'];
|
|
89
|
+
const text = [...base, 'contains', 'startsWith', 'endsWith', 'mode'];
|
|
90
|
+
const operators = {
|
|
91
|
+
string: text,
|
|
92
|
+
number: ordered,
|
|
93
|
+
decimal: ordered,
|
|
94
|
+
dateTime: ordered,
|
|
95
|
+
date: ordered,
|
|
96
|
+
boolean: base,
|
|
97
|
+
// Any JSON, a path into it or not, typed field or not (D-149).
|
|
98
|
+
json: [
|
|
99
|
+
'path',
|
|
100
|
+
...ordered,
|
|
101
|
+
'stringContains',
|
|
102
|
+
'stringStartsWith',
|
|
103
|
+
'stringEndsWith',
|
|
104
|
+
'mode',
|
|
105
|
+
'arrayContains',
|
|
106
|
+
'arrayStartsWith',
|
|
107
|
+
'arrayEndsWith',
|
|
108
|
+
],
|
|
109
|
+
enum: base,
|
|
110
|
+
};
|
|
111
|
+
/** `StringFilter`, `PostStatusFilter`: `equals` and `not` take `null` too. */
|
|
112
|
+
function filter(doc, name, value, list) {
|
|
113
|
+
return doc.components.ref(`${name}Filter`, () => objectOf(Object.fromEntries(list.map((operator) => {
|
|
114
|
+
if (operator === 'mode') {
|
|
115
|
+
const mode = doc.components.ref('QueryMode', () => ({
|
|
116
|
+
type: 'string',
|
|
117
|
+
enum: ['insensitive'],
|
|
118
|
+
}));
|
|
119
|
+
return [operator, mode];
|
|
120
|
+
}
|
|
121
|
+
if (operator === 'in' || operator === 'notIn') {
|
|
122
|
+
return [operator, arrayOf(value)];
|
|
123
|
+
}
|
|
124
|
+
if (operator === 'path')
|
|
125
|
+
return [operator, arrayOf({ type: 'string' })];
|
|
126
|
+
if (operator.startsWith('string')) {
|
|
127
|
+
return [operator, { type: 'string' }];
|
|
128
|
+
}
|
|
129
|
+
const nullable = operator === 'equals' || operator === 'not';
|
|
130
|
+
return [operator, nullable ? orNull(value) : value];
|
|
131
|
+
}))));
|
|
132
|
+
}
|
|
133
|
+
function scalarFilter(doc, model, field) {
|
|
134
|
+
// A typed json field's operands are any JSON at a path (D-149).
|
|
135
|
+
if (field.type === 'json' && !field.list) {
|
|
136
|
+
return filter(doc, 'JSON', {}, operators.json);
|
|
137
|
+
}
|
|
138
|
+
const { name, schema } = item(doc, model, field, 'input');
|
|
139
|
+
if (field.list) {
|
|
140
|
+
return doc.components.ref(`${name}ListFilter`, () => objectOf({
|
|
141
|
+
has: schema,
|
|
142
|
+
hasSome: arrayOf(schema),
|
|
143
|
+
hasEvery: arrayOf(schema),
|
|
144
|
+
isEmpty: { type: 'boolean' },
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
return filter(doc, name, schema, operators[field.type]);
|
|
148
|
+
}
|
|
149
|
+
/** One value or a list: GraphQL takes either for a list argument. */
|
|
150
|
+
const oneOrMore = (schema) => ({
|
|
151
|
+
anyOf: [schema, arrayOf(schema)],
|
|
152
|
+
});
|
|
153
|
+
/** `PostWhereInput`, its relation filters included. */
|
|
154
|
+
export function whereOf(doc, model) {
|
|
155
|
+
return doc.components.ref(`${model}WhereInput`, () => {
|
|
156
|
+
const self = refTo(`${model}WhereInput`);
|
|
157
|
+
const entry = entryOf(doc.api, model);
|
|
158
|
+
const dates = filter(doc, 'DateTime', dateTime, ordered);
|
|
159
|
+
const fields = {
|
|
160
|
+
AND: oneOrMore(self),
|
|
161
|
+
OR: oneOrMore(self),
|
|
162
|
+
NOT: oneOrMore(self),
|
|
163
|
+
id: filter(doc, 'ID', id, base),
|
|
164
|
+
createdAt: dates,
|
|
165
|
+
updatedAt: dates,
|
|
166
|
+
};
|
|
167
|
+
if (searched(readable(entry)))
|
|
168
|
+
fields['_search'] = { type: 'string' };
|
|
169
|
+
for (const field of readable(entry)) {
|
|
170
|
+
// Only a beforeFind filters a plugin field (D-122).
|
|
171
|
+
if (entry.exposed.fieldTypes[field.name] !== undefined)
|
|
172
|
+
continue;
|
|
173
|
+
fields[field.name] =
|
|
174
|
+
field.kind === 'scalar'
|
|
175
|
+
? scalarFilter(doc, model, field)
|
|
176
|
+
: relationFilter(doc, field);
|
|
177
|
+
}
|
|
178
|
+
return objectOf(fields);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
function relationFilter(doc, field) {
|
|
182
|
+
const one = field.kind === 'one';
|
|
183
|
+
const name = `${field.target}${one ? 'Relation' : 'ListRelation'}Filter`;
|
|
184
|
+
return doc.components.ref(name, () => {
|
|
185
|
+
const where = whereOf(doc, field.target);
|
|
186
|
+
const keys = one ? ['is', 'isNot'] : ['some', 'every', 'none'];
|
|
187
|
+
return objectOf(Object.fromEntries(keys.map((key) => [key, where])));
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
/** `PostOrderByInput`, an object or a list of them (D-94, D-134). */
|
|
191
|
+
export function orderByOf(doc, model) {
|
|
192
|
+
return doc.components.ref(`${model}OrderByInput`, () => {
|
|
193
|
+
const order = doc.components.ref('SortOrder', () => ({
|
|
194
|
+
type: 'string',
|
|
195
|
+
enum: ['asc', 'desc'],
|
|
196
|
+
}));
|
|
197
|
+
const entry = entryOf(doc.api, model);
|
|
198
|
+
const fields = {
|
|
199
|
+
id: order,
|
|
200
|
+
createdAt: order,
|
|
201
|
+
updatedAt: order,
|
|
202
|
+
};
|
|
203
|
+
// The kernel refuses it in a relation's: a read ranks by its own search.
|
|
204
|
+
if (searched(readable(entry)))
|
|
205
|
+
fields['_relevance'] = order;
|
|
206
|
+
for (const field of readable(entry)) {
|
|
207
|
+
if (field.kind === 'one')
|
|
208
|
+
fields[field.name] = orderByOf(doc, field.target);
|
|
209
|
+
else if (field.kind === 'many') {
|
|
210
|
+
fields[field.name] = doc.components.ref('CountOrderByInput', () => objectOf({ _count: order }));
|
|
211
|
+
}
|
|
212
|
+
else if (field.type !== 'json' &&
|
|
213
|
+
!field.list &&
|
|
214
|
+
entry.exposed.fieldTypes[field.name] === undefined) {
|
|
215
|
+
fields[field.name] = order;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return objectOf(fields);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* `PostWhereUniqueInput`, as REST takes it: the fields of one unique set,
|
|
223
|
+
* `{ id }` or `{ author, slug }`, a relation by its target's id.
|
|
224
|
+
*/
|
|
225
|
+
export function uniqueOf(doc, model) {
|
|
226
|
+
return doc.components.ref(`${model}WhereUniqueInput`, () => {
|
|
227
|
+
const entry = entryOf(doc.api, model);
|
|
228
|
+
const { readRules, uniques } = entry.exposed;
|
|
229
|
+
// A field only Master reads is absent, as from the filters (D-44).
|
|
230
|
+
const sets = uniques.filter((unique) => unique.every((name) => readRules[name] !== 'private'));
|
|
231
|
+
const typeOf = (name) => {
|
|
232
|
+
const field = entry.model.fields.find((each) => each.name === name);
|
|
233
|
+
return field?.kind === 'scalar' ? valueOf(doc, entry, field, 'input') : id;
|
|
234
|
+
};
|
|
235
|
+
return {
|
|
236
|
+
anyOf: sets.map((unique) => objectOf(Object.fromEntries(unique.map((name) => [name, typeOf(name)])), unique)),
|
|
237
|
+
};
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
/** Exactly one of an object's keys, as a GraphQL `@oneOf` input. */
|
|
241
|
+
const oneKey = (schema) => ({
|
|
242
|
+
...schema,
|
|
243
|
+
minProperties: 1,
|
|
244
|
+
maxProperties: 1,
|
|
245
|
+
});
|
|
246
|
+
/**
|
|
247
|
+
* A relation's write, as GraphQL names it (D-128, D-138): a hidden target is
|
|
248
|
+
* connected, disconnected or set, never created, updated or deleted.
|
|
249
|
+
*/
|
|
250
|
+
function relationInput(doc, field, creating) {
|
|
251
|
+
const many = field.kind === 'many';
|
|
252
|
+
const shape = many ? 'Many' : field.optional ? 'One' : 'OneRequired';
|
|
253
|
+
const facing = `Without${upper(field.inverse)}`;
|
|
254
|
+
const name = creating
|
|
255
|
+
? `${field.target}CreateNested${many ? 'Many' : 'One'}${facing}Input`
|
|
256
|
+
: `${field.target}Update${shape}${facing}NestedInput`;
|
|
257
|
+
return doc.components.ref(name, () => {
|
|
258
|
+
const listed = (schema) => (many ? arrayOf(schema) : schema);
|
|
259
|
+
const unique = uniqueOf(doc, field.target);
|
|
260
|
+
const { visible } = entryOf(doc.api, field.target);
|
|
261
|
+
const updating = !creating && visible;
|
|
262
|
+
const fields = { connect: listed(unique) };
|
|
263
|
+
if (visible) {
|
|
264
|
+
const created = dataOf(doc, field.target, true, field.inverse);
|
|
265
|
+
const orCreate = doc.components.ref(`${field.target}CreateOrConnect${facing}Input`, () => objectOf({ where: unique, create: created }, ['where', 'create']));
|
|
266
|
+
fields['connectOrCreate'] = listed(orCreate);
|
|
267
|
+
fields['create'] = listed(created);
|
|
268
|
+
}
|
|
269
|
+
if (updating && shape !== 'OneRequired') {
|
|
270
|
+
fields['delete'] = many ? listed(unique) : { type: 'boolean' };
|
|
271
|
+
}
|
|
272
|
+
if (!creating && shape !== 'OneRequired') {
|
|
273
|
+
fields['disconnect'] = many ? listed(unique) : { type: 'boolean' };
|
|
274
|
+
}
|
|
275
|
+
// A filtered relation takes no `set` (D-151).
|
|
276
|
+
if (!creating && many && field.filtered !== true) {
|
|
277
|
+
fields['set'] = listed(unique);
|
|
278
|
+
}
|
|
279
|
+
if (updating) {
|
|
280
|
+
const data = dataOf(doc, field.target, false, field.inverse);
|
|
281
|
+
fields['update'] = many
|
|
282
|
+
? listed(doc.components.ref(`${field.target}UpdateWithWhereUnique${facing}Input`, () => objectOf({ where: unique, data }, ['where', 'data'])))
|
|
283
|
+
: data;
|
|
284
|
+
}
|
|
285
|
+
return many ? objectOf(fields) : oneKey(objectOf(fields));
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* `PostCreateInput` and `PostUpdateInput`: every field, those only Master
|
|
290
|
+
* reads included; a create needs the fields `create` needs (D-53). Through
|
|
291
|
+
* a relation, `CommentCreateWithoutPostInput`, without it (D-138).
|
|
292
|
+
*/
|
|
293
|
+
export function dataOf(doc, model, creating, without) {
|
|
294
|
+
const facing = without === undefined ? '' : `Without${upper(without)}`;
|
|
295
|
+
const name = `${model}${creating ? 'Create' : 'Update'}${facing}Input`;
|
|
296
|
+
return doc.components.ref(name, () => {
|
|
297
|
+
const entry = entryOf(doc.api, model);
|
|
298
|
+
const fields = {};
|
|
299
|
+
for (const field of entry.model.fields) {
|
|
300
|
+
// A view is read-only (D-151).
|
|
301
|
+
if (field.name === without ||
|
|
302
|
+
(field.kind === 'many' && field.from !== undefined))
|
|
303
|
+
continue;
|
|
304
|
+
if (field.kind !== 'scalar') {
|
|
305
|
+
fields[field.name] = relationInput(doc, field, creating);
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
const value = valueOf(doc, entry, field, 'input');
|
|
309
|
+
fields[field.name] = field.optional && !field.list ? orNull(value) : value;
|
|
310
|
+
}
|
|
311
|
+
const required = creating
|
|
312
|
+
? entry.exposed.required.filter((each) => each !== without)
|
|
313
|
+
: [];
|
|
314
|
+
return objectOf(described(fields, entry.exposed.descriptions), required);
|
|
315
|
+
});
|
|
316
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Entry } from './entries.ts';
|
|
2
|
+
import type { Doc } from './openapi-inputs.ts';
|
|
3
|
+
import type { Schema } from './openapi.ts';
|
|
4
|
+
/**
|
|
5
|
+
* `Post`: its default fields, which a row always holds without `select`,
|
|
6
|
+
* and its relations, which `include` adds. A field with a read rule may be
|
|
7
|
+
* `null`, a `one` relation always may (D-44).
|
|
8
|
+
*/
|
|
9
|
+
export declare function rowOf(doc: Doc, model: string): Schema;
|
|
10
|
+
/** `/posts` and `/posts/{id}`, as REST serves them (D-129, D-142). */
|
|
11
|
+
export declare function modelPaths(doc: Doc, entry: Entry): {
|
|
12
|
+
[path: string]: Schema;
|
|
13
|
+
};
|
|
14
|
+
//# sourceMappingURL=openapi-models.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openapi-models.d.ts","sourceRoot":"","sources":["../src/openapi-models.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,cAAc,CAAA;AAEzC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,qBAAqB,CAAA;AAS9C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AA+C1C;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAoDrD;AAyJD,sEAAsE;AACtE,wBAAgB,UAAU,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,KAAK,GAAG;IAAE,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,CAwF7E"}
|