@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/CHANGELOG.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# @ultimabase/rest
|
|
2
|
+
|
|
3
|
+
## 0.1.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- The published starter starts: `ultimabase dev` runs `dist/serve.js`, and `@ultimabase/realtime`, which `@ultimabase/graphql` and `@ultimabase/rest` import, is a required peer the starter lists.
|
|
8
|
+
- Updated dependencies
|
|
9
|
+
- ultimabase@0.1.1
|
|
10
|
+
- @ultimabase/http@0.1.1
|
|
11
|
+
- @ultimabase/realtime@0.1.1
|
|
12
|
+
|
|
13
|
+
## 0.1.0
|
|
14
|
+
|
|
15
|
+
### Minor Changes
|
|
16
|
+
|
|
17
|
+
- 0e885c4: The first release: the kernel, its adapters, transports and plugins, the testing helpers and the starter.
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- Updated dependencies [0e885c4]
|
|
22
|
+
- @ultimabase/http@0.1.0
|
|
23
|
+
- @ultimabase/realtime@0.1.0
|
|
24
|
+
- ultimabase@0.1.0
|
package/README.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @ultimabase/rest
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A REST API generated from the models and the functions, served by `@ultimabase/http`. It is the GraphQL API in HTTP's grammar: the same names, operators and answers.
|
|
4
|
+
|
|
5
|
+
Documentation: [REST](https://ultimabase.dev/docs/rest).
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ExposedModel, SchemaModel } from 'ultimabase';
|
|
2
|
+
/** A model's key of `.with` (D-129). */
|
|
3
|
+
export type RestWith = {
|
|
4
|
+
readonly plural?: string;
|
|
5
|
+
readonly maxTake?: number;
|
|
6
|
+
} | false;
|
|
7
|
+
export type Args = {
|
|
8
|
+
readonly [key: string]: unknown;
|
|
9
|
+
};
|
|
10
|
+
export type Entry = {
|
|
11
|
+
readonly exposed: ExposedModel<RestWith>;
|
|
12
|
+
readonly model: SchemaModel;
|
|
13
|
+
/** Routed; a hidden model is read and connected through relations only. */
|
|
14
|
+
readonly visible: boolean;
|
|
15
|
+
readonly plural: string;
|
|
16
|
+
/** The rows a list returns without `first`, `last` or `take`. */
|
|
17
|
+
readonly take: number;
|
|
18
|
+
readonly maxTake: number;
|
|
19
|
+
};
|
|
20
|
+
/** What every route reads: the models and the plugin's settings. */
|
|
21
|
+
export type Api = {
|
|
22
|
+
readonly entries: ReadonlyMap<string, Entry>;
|
|
23
|
+
readonly pagination: 'connection' | 'list';
|
|
24
|
+
readonly take: number;
|
|
25
|
+
readonly maxDepth: number;
|
|
26
|
+
readonly maxRows: number;
|
|
27
|
+
};
|
|
28
|
+
export declare function entryOf(api: Api, model: string): Entry;
|
|
29
|
+
/** Stops startup on a route name GraphQL would refuse as a field name. */
|
|
30
|
+
export declare function checkName(name: string, where: string): string;
|
|
31
|
+
/** `id`, `createdAt` and `updatedAt`, which every model has (D-22). */
|
|
32
|
+
export declare const system: readonly string[];
|
|
33
|
+
/** Each model's entry; two routed models of one plural stop startup. */
|
|
34
|
+
export declare function entriesOf(models: readonly ExposedModel<RestWith>[], options: {
|
|
35
|
+
readonly take?: number;
|
|
36
|
+
readonly maxTake?: number;
|
|
37
|
+
}): ReadonlyMap<string, Entry>;
|
|
38
|
+
//# sourceMappingURL=entries.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entries.d.ts","sourceRoot":"","sources":["../src/entries.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAG3D,wCAAwC;AACxC,MAAM,MAAM,QAAQ,GACnB;IAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,KAAK,CAAA;AAEhE,MAAM,MAAM,IAAI,GAAG;IAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,CAAA;AAEtD,MAAM,MAAM,KAAK,GAAG;IACnB,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC,QAAQ,CAAC,CAAA;IACxC,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAA;IAC3B,2EAA2E;IAC3E,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,iEAAiE;IACjE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACxB,CAAA;AAED,oEAAoE;AACpE,MAAM,MAAM,GAAG,GAAG;IACjB,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,KAAK,CAAC,CAAA;IAC5C,QAAQ,CAAC,UAAU,EAAE,YAAY,GAAG,MAAM,CAAA;IAC1C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACxB,CAAA;AAED,wBAAgB,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,GAAG,KAAK,CAItD;AAED,0EAA0E;AAC1E,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAK7D;AAED,uEAAuE;AACvE,eAAO,MAAM,MAAM,EAAE,SAAS,MAAM,EAAqC,CAAA;AAEzE,wEAAwE;AACxE,wBAAgB,SAAS,CACxB,MAAM,EAAE,SAAS,YAAY,CAAC,QAAQ,CAAC,EAAE,EACzC,OAAO,EAAE;IAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAC5D,WAAW,CAAC,MAAM,EAAE,KAAK,CAAC,CA4B5B"}
|
package/dist/entries.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// The models REST serves (D-129): each model's plural and limits, named and
|
|
2
|
+
// bounded as GraphQL names and bounds them (D-107).
|
|
3
|
+
import { plural } from '@ultimabase/http/internal';
|
|
4
|
+
import { Refused } from 'ultimabase';
|
|
5
|
+
export function entryOf(api, model) {
|
|
6
|
+
const entry = api.entries.get(model);
|
|
7
|
+
if (entry === undefined)
|
|
8
|
+
throw new Error(`Unknown model ${model}.`);
|
|
9
|
+
return entry;
|
|
10
|
+
}
|
|
11
|
+
/** Stops startup on a route name GraphQL would refuse as a field name. */
|
|
12
|
+
export function checkName(name, where) {
|
|
13
|
+
if (/^[_A-Za-z][_0-9A-Za-z]*$/.test(name) && !name.startsWith('__')) {
|
|
14
|
+
return name;
|
|
15
|
+
}
|
|
16
|
+
throw new Refused(`${where}: ${name} is not a route name.`);
|
|
17
|
+
}
|
|
18
|
+
/** `id`, `createdAt` and `updatedAt`, which every model has (D-22). */
|
|
19
|
+
export const system = ['id', 'createdAt', 'updatedAt'];
|
|
20
|
+
/** Each model's entry; two routed models of one plural stop startup. */
|
|
21
|
+
export function entriesOf(models, options) {
|
|
22
|
+
const take = options.take ?? 100;
|
|
23
|
+
const entries = new Map();
|
|
24
|
+
const owners = new Map();
|
|
25
|
+
for (const exposed of models) {
|
|
26
|
+
const name = exposed.schema.name;
|
|
27
|
+
const own = exposed.with === false ? {} : (exposed.with ?? {});
|
|
28
|
+
const given = own.plural ?? plural(name.charAt(0).toLowerCase() + name.slice(1));
|
|
29
|
+
const entry = {
|
|
30
|
+
exposed,
|
|
31
|
+
model: exposed.schema,
|
|
32
|
+
visible: exposed.with !== false,
|
|
33
|
+
plural: checkName(given, name),
|
|
34
|
+
take,
|
|
35
|
+
maxTake: own.maxTake ?? options.maxTake ?? 100,
|
|
36
|
+
};
|
|
37
|
+
entries.set(name, entry);
|
|
38
|
+
if (!entry.visible)
|
|
39
|
+
continue;
|
|
40
|
+
const other = given === 'functions' ? 'the functions' : owners.get(given);
|
|
41
|
+
if (other !== undefined) {
|
|
42
|
+
throw new Refused(`${name}: the route ${given} is ${other}'s; set .with({ rest: { plural } }).`);
|
|
43
|
+
}
|
|
44
|
+
owners.set(given, name);
|
|
45
|
+
}
|
|
46
|
+
return entries;
|
|
47
|
+
}
|
package/dist/fields.d.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { Relay } from '@ultimabase/http/internal';
|
|
2
|
+
import type { Row } from 'ultimabase';
|
|
3
|
+
import type { Api, Args, Entry } from './entries.ts';
|
|
4
|
+
/** A field of the response, in the order the request wrote it. */
|
|
5
|
+
type Out = {
|
|
6
|
+
readonly kind: 'scalar';
|
|
7
|
+
readonly name: string;
|
|
8
|
+
readonly toJSON: ((value: unknown) => unknown) | undefined;
|
|
9
|
+
} | {
|
|
10
|
+
readonly kind: 'one';
|
|
11
|
+
readonly name: string;
|
|
12
|
+
readonly shape: Shape;
|
|
13
|
+
} | {
|
|
14
|
+
readonly kind: 'many';
|
|
15
|
+
readonly name: string;
|
|
16
|
+
readonly shape: Shape;
|
|
17
|
+
readonly relay: Relay | undefined;
|
|
18
|
+
/** The rows it asks for, which the row budget counts. */
|
|
19
|
+
readonly size: number;
|
|
20
|
+
/** Whether it answers its `totalCount` (D-134). */
|
|
21
|
+
readonly counted: boolean;
|
|
22
|
+
} | {
|
|
23
|
+
/** A virtual's rows of a model, re-read by the kernel (D-151). */
|
|
24
|
+
readonly kind: 'returned';
|
|
25
|
+
readonly name: string;
|
|
26
|
+
readonly owner: string;
|
|
27
|
+
readonly shape: Shape;
|
|
28
|
+
readonly list: boolean;
|
|
29
|
+
/** The rows it counts: `take` for a list, as a function's (D-109). */
|
|
30
|
+
readonly size: number;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* What a row of the response holds. `sorted` is the relations its list's
|
|
34
|
+
* `orderBy` goes through, each a level of `maxDepth` (D-134).
|
|
35
|
+
*/
|
|
36
|
+
export type Shape = {
|
|
37
|
+
readonly fields: readonly Out[];
|
|
38
|
+
readonly sorted?: number;
|
|
39
|
+
};
|
|
40
|
+
/** A row's fields and its relations' reads, as the kernel takes them. */
|
|
41
|
+
export type Read = {
|
|
42
|
+
readonly read: Args;
|
|
43
|
+
readonly shape: Shape;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* `id`, the dates, each scalar field a caller other than Master reads, and
|
|
47
|
+
* each virtual of a scalar without an input (D-151).
|
|
48
|
+
*/
|
|
49
|
+
export declare const defaultShape: (entry: Entry) => Shape;
|
|
50
|
+
/** The virtuals a row holds without `select` (D-151). */
|
|
51
|
+
export declare const defaultVirtuals: (entry: Entry) => readonly string[];
|
|
52
|
+
/** A list's `where`, its dates read; `null` is absent, as in GraphQL. */
|
|
53
|
+
export declare const filterOf: (api: Api, entry: Entry, where: unknown, path: string) => Args;
|
|
54
|
+
/** Reads `select` or `include` as the request wrote them. */
|
|
55
|
+
declare class Reader {
|
|
56
|
+
#private;
|
|
57
|
+
constructor(api: Api, master: boolean);
|
|
58
|
+
/** A row's `select` or `include`, under `path` (`''` at the root). */
|
|
59
|
+
fields(entry: Entry, args: Args, path: string): Read;
|
|
60
|
+
/** A list's arguments: its page, filter, order and fields. */
|
|
61
|
+
list(entry: Entry, args: Args, path: string): Read & {
|
|
62
|
+
readonly relay: Relay | undefined;
|
|
63
|
+
readonly size: number;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** The reader of a request's `select` and `include`. */
|
|
67
|
+
export declare const readerOf: (api: Api, master: boolean) => Reader;
|
|
68
|
+
/** The levels of relations a row nests, the row counting one. */
|
|
69
|
+
export declare function depthOf(shape: Shape): number;
|
|
70
|
+
/** The rows the relations of `times` rows may return, as D-107 counts them. */
|
|
71
|
+
export declare function rowsBelow(shape: Shape, times: number): number;
|
|
72
|
+
/** A row as GraphQL returns it: its fields, its relations' pages. */
|
|
73
|
+
export declare function shaped(row: Row, shape: Shape): {
|
|
74
|
+
[field: string]: unknown;
|
|
75
|
+
};
|
|
76
|
+
export {};
|
|
77
|
+
//# sourceMappingURL=fields.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fields.d.ts","sourceRoot":"","sources":["../src/fields.ts"],"names":[],"mappings":"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"}
|
package/dist/fields.js
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { connection, pageArgs, pageOf, readable, readableVirtuals, rowOf, rowsOf, } from '@ultimabase/http/internal';
|
|
2
|
+
import { InvalidQuery } from 'ultimabase';
|
|
3
|
+
import { entryOf, system } from './entries.js';
|
|
4
|
+
import { isArgs, whereOf } from './values.js';
|
|
5
|
+
/** The relations an `orderBy` goes through, the deepest path's. */
|
|
6
|
+
function sortDepth(orderBy) {
|
|
7
|
+
if (Array.isArray(orderBy))
|
|
8
|
+
return Math.max(0, ...orderBy.map(sortDepth));
|
|
9
|
+
if (typeof orderBy !== 'object' || orderBy === null)
|
|
10
|
+
return 0;
|
|
11
|
+
return Math.max(0, ...Object.values(orderBy).map((value) => typeof value === 'object' && value !== null ? 1 + sortDepth(value) : 0));
|
|
12
|
+
}
|
|
13
|
+
const unknownKey = (path, model) => new InvalidQuery(path, `Unknown key '${path}' on ${model}.`);
|
|
14
|
+
const scalar = (entry, name) => ({
|
|
15
|
+
kind: 'scalar',
|
|
16
|
+
name,
|
|
17
|
+
// A plugin field reads as its toJSON gives (D-122).
|
|
18
|
+
toJSON: entry.exposed.fieldTypes[name]?.toJSON,
|
|
19
|
+
});
|
|
20
|
+
/**
|
|
21
|
+
* `id`, the dates, each scalar field a caller other than Master reads, and
|
|
22
|
+
* each virtual of a scalar without an input (D-151).
|
|
23
|
+
*/
|
|
24
|
+
export const defaultShape = (entry) => ({
|
|
25
|
+
fields: [
|
|
26
|
+
...system.map((name) => scalar(entry, name)),
|
|
27
|
+
...readable(entry)
|
|
28
|
+
.filter((field) => field.kind === 'scalar')
|
|
29
|
+
.map((field) => scalar(entry, field.name)),
|
|
30
|
+
...defaultVirtuals(entry).map((name) => scalar(entry, name)),
|
|
31
|
+
],
|
|
32
|
+
});
|
|
33
|
+
/** The virtuals a row holds without `select` (D-151). */
|
|
34
|
+
export const defaultVirtuals = (entry) => readableVirtuals(entry)
|
|
35
|
+
.filter((virtual) => virtual.default)
|
|
36
|
+
.map((virtual) => virtual.name);
|
|
37
|
+
// `take` for none or a negative number; above `maxTake`, the list refuses.
|
|
38
|
+
const sizeOf = (entry, asked) => typeof asked === 'number' && asked >= 0
|
|
39
|
+
? Math.min(asked, entry.maxTake)
|
|
40
|
+
: entry.take;
|
|
41
|
+
/** A list's `where`, its dates read; `null` is absent, as in GraphQL. */
|
|
42
|
+
export const filterOf = (api, entry, where, path) => where === undefined || where === null
|
|
43
|
+
? {}
|
|
44
|
+
: {
|
|
45
|
+
where: isArgs(where)
|
|
46
|
+
? whereOf(api, entry.model.name, where, path)
|
|
47
|
+
: where,
|
|
48
|
+
};
|
|
49
|
+
/** Reads `select` or `include` as the request wrote them. */
|
|
50
|
+
class Reader {
|
|
51
|
+
#api;
|
|
52
|
+
#master;
|
|
53
|
+
constructor(api, master) {
|
|
54
|
+
this.#api = api;
|
|
55
|
+
this.#master = master;
|
|
56
|
+
}
|
|
57
|
+
/** A row's `select` or `include`, under `path` (`''` at the root). */
|
|
58
|
+
fields(entry, args, path) {
|
|
59
|
+
const at = (key) => (path === '' ? key : `${path}.${key}`);
|
|
60
|
+
const { select, include } = args;
|
|
61
|
+
if (select !== undefined && include !== undefined) {
|
|
62
|
+
throw new InvalidQuery(path, `${at('select')} and include cannot be used together.`);
|
|
63
|
+
}
|
|
64
|
+
const base = select === undefined ? 'include' : 'select';
|
|
65
|
+
const chosen = select ?? include;
|
|
66
|
+
if (chosen === undefined)
|
|
67
|
+
return { read: {}, shape: defaultShape(entry) };
|
|
68
|
+
if (!isArgs(chosen)) {
|
|
69
|
+
throw new InvalidQuery(at(base), `${at(base)} must be an object.`);
|
|
70
|
+
}
|
|
71
|
+
const kernel = base === 'select' ? { id: true } : {};
|
|
72
|
+
const fields = base === 'select' ? [] : [...defaultShape(entry).fields];
|
|
73
|
+
// The relations' `totalCount`, each the count of its `where` (D-134).
|
|
74
|
+
const counts = {};
|
|
75
|
+
const model = entry.model.name;
|
|
76
|
+
for (const [name, value] of Object.entries(chosen)) {
|
|
77
|
+
const where = `${at(base)}.${name}`;
|
|
78
|
+
const virtual = readableVirtuals(entry).find((each) => each.name === name);
|
|
79
|
+
if (virtual !== undefined) {
|
|
80
|
+
if (value === false)
|
|
81
|
+
continue;
|
|
82
|
+
const read = this.#virtual(entry, virtual, value, where);
|
|
83
|
+
kernel[name] = read.read;
|
|
84
|
+
// `include` reads a default virtual again, where it is.
|
|
85
|
+
const known = fields.findIndex((each) => each.name === name);
|
|
86
|
+
if (known === -1)
|
|
87
|
+
fields.push(read.field);
|
|
88
|
+
else
|
|
89
|
+
fields[known] = read.field;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const field = readable(entry).find((each) => each.name === name);
|
|
93
|
+
const isScalar = field === undefined ? system.includes(name) : field.kind === 'scalar';
|
|
94
|
+
if ((field === undefined && !isScalar) ||
|
|
95
|
+
(base === 'include' && isScalar)) {
|
|
96
|
+
throw unknownKey(where, model);
|
|
97
|
+
}
|
|
98
|
+
if (value === false)
|
|
99
|
+
continue;
|
|
100
|
+
if (field === undefined || field.kind === 'scalar') {
|
|
101
|
+
if (value !== true) {
|
|
102
|
+
throw new InvalidQuery(where, `${where} must be a boolean.`);
|
|
103
|
+
}
|
|
104
|
+
kernel[name] = true;
|
|
105
|
+
fields.push(scalar(entry, name));
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const target = entryOf(this.#api, field.target);
|
|
109
|
+
const given = value === true ? {} : value;
|
|
110
|
+
if (!isArgs(given)) {
|
|
111
|
+
throw new InvalidQuery(where, `${where} must be an object.`);
|
|
112
|
+
}
|
|
113
|
+
if (field.kind === 'one') {
|
|
114
|
+
this.#keys(given, ['select', 'include'], where, model);
|
|
115
|
+
const nested = this.fields(target, given, where);
|
|
116
|
+
kernel[name] = nested.read;
|
|
117
|
+
fields.push({ kind: 'one', name, shape: nested.shape });
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
// A list has no count (D-134): `totalCount` is unknown there.
|
|
121
|
+
const pages = this.#api.pagination === 'connection';
|
|
122
|
+
const { totalCount, ...listed } = given;
|
|
123
|
+
const many = this.list(target, pages ? listed : given, where);
|
|
124
|
+
const counted = pages && totalCount === true;
|
|
125
|
+
if (pages &&
|
|
126
|
+
totalCount !== undefined &&
|
|
127
|
+
typeof totalCount !== 'boolean') {
|
|
128
|
+
const key = `${where}.totalCount`;
|
|
129
|
+
throw new InvalidQuery(key, `${key} must be true or false.`);
|
|
130
|
+
}
|
|
131
|
+
if (counted) {
|
|
132
|
+
const filter = many.read['where'];
|
|
133
|
+
counts[name] = filter === undefined ? true : { where: filter };
|
|
134
|
+
}
|
|
135
|
+
kernel[name] = many.read;
|
|
136
|
+
fields.push({
|
|
137
|
+
kind: 'many',
|
|
138
|
+
name,
|
|
139
|
+
shape: many.shape,
|
|
140
|
+
relay: many.relay,
|
|
141
|
+
// A count reads the rows it counts, however few the page returns.
|
|
142
|
+
size: counted ? Math.max(many.size, target.take) : many.size,
|
|
143
|
+
counted,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
if (Object.keys(counts).length > 0)
|
|
147
|
+
kernel['_count'] = { select: counts };
|
|
148
|
+
return { read: { [base]: kernel }, shape: { fields } };
|
|
149
|
+
}
|
|
150
|
+
/** A virtual's read: `true` or its input, a model's fields too (D-151). */
|
|
151
|
+
#virtual(entry, virtual, value, where) {
|
|
152
|
+
const { name, output } = virtual;
|
|
153
|
+
if (output.kind !== 'model') {
|
|
154
|
+
return { read: value, field: scalar(entry, name) };
|
|
155
|
+
}
|
|
156
|
+
if (value !== true && !isArgs(value)) {
|
|
157
|
+
throw new InvalidQuery(where, `${where} must be true or an object.`);
|
|
158
|
+
}
|
|
159
|
+
const { select, include, ...input } = value === true ? {} : value;
|
|
160
|
+
const target = entryOf(this.#api, output.name);
|
|
161
|
+
const nested = this.fields(target, { select, include }, where);
|
|
162
|
+
const list = output.form === 'list';
|
|
163
|
+
return {
|
|
164
|
+
read: value === true ? true : { ...input, ...nested.read },
|
|
165
|
+
field: {
|
|
166
|
+
kind: 'returned',
|
|
167
|
+
name,
|
|
168
|
+
owner: `${entry.model.name}.${name}`,
|
|
169
|
+
shape: nested.shape,
|
|
170
|
+
list,
|
|
171
|
+
size: list ? this.#api.take : 1,
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/** A list's arguments: its page, filter, order and fields. */
|
|
176
|
+
list(entry, args, path) {
|
|
177
|
+
const at = (key) => (path === '' ? key : `${path}.${key}`);
|
|
178
|
+
const paging = pageArgs(this.#api.pagination);
|
|
179
|
+
const keys = ['select', 'include', 'where', 'orderBy', ...paging];
|
|
180
|
+
this.#keys(args, keys, path, entry.model.name);
|
|
181
|
+
const page = pageOf(this.#api.pagination, entry, args, this.#master, at);
|
|
182
|
+
const { where, orderBy } = args;
|
|
183
|
+
const nested = this.fields(entry, args, path);
|
|
184
|
+
const asked = this.#api.pagination === 'list'
|
|
185
|
+
? args['take']
|
|
186
|
+
: (args['last'] ?? args['first']);
|
|
187
|
+
return {
|
|
188
|
+
read: {
|
|
189
|
+
...nested.read,
|
|
190
|
+
...page.read,
|
|
191
|
+
...filterOf(this.#api, entry, where, at('where')),
|
|
192
|
+
...(orderBy === undefined || orderBy === null ? {} : { orderBy }),
|
|
193
|
+
},
|
|
194
|
+
shape: { ...nested.shape, sorted: sortDepth(orderBy) },
|
|
195
|
+
relay: page.relay,
|
|
196
|
+
size: sizeOf(entry, asked),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
#keys(args, keys, path, model) {
|
|
200
|
+
for (const key of Object.keys(args)) {
|
|
201
|
+
if (!keys.includes(key)) {
|
|
202
|
+
throw unknownKey(path === '' ? key : `${path}.${key}`, model);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/** The reader of a request's `select` and `include`. */
|
|
208
|
+
export const readerOf = (api, master) => new Reader(api, master);
|
|
209
|
+
/** The levels of relations a row nests, the row counting one. */
|
|
210
|
+
export function depthOf(shape) {
|
|
211
|
+
let deepest = shape.sorted ?? 0;
|
|
212
|
+
for (const field of shape.fields) {
|
|
213
|
+
if (field.kind !== 'scalar')
|
|
214
|
+
deepest = Math.max(deepest, depthOf(field.shape));
|
|
215
|
+
}
|
|
216
|
+
return 1 + deepest;
|
|
217
|
+
}
|
|
218
|
+
/** The rows the relations of `times` rows may return, as D-107 counts them. */
|
|
219
|
+
export function rowsBelow(shape, times) {
|
|
220
|
+
let rows = 0;
|
|
221
|
+
for (const field of shape.fields) {
|
|
222
|
+
if (field.kind === 'scalar')
|
|
223
|
+
continue;
|
|
224
|
+
const each = field.kind === 'one' ? times : times * field.size;
|
|
225
|
+
rows += each;
|
|
226
|
+
if (each > 0)
|
|
227
|
+
rows += rowsBelow(field.shape, each);
|
|
228
|
+
}
|
|
229
|
+
return rows;
|
|
230
|
+
}
|
|
231
|
+
/** The count the kernel read of a row's relation (D-134). */
|
|
232
|
+
function countOf(row, name) {
|
|
233
|
+
const counts = row['_count'];
|
|
234
|
+
return isArgs(counts) ? counts[name] : undefined;
|
|
235
|
+
}
|
|
236
|
+
/** A virtual's rows of a model, at most `take` under a relation (D-109). */
|
|
237
|
+
function returned(field, value) {
|
|
238
|
+
if (!field.list) {
|
|
239
|
+
const row = rowOf(value);
|
|
240
|
+
return row === undefined ? null : shaped(row, field.shape);
|
|
241
|
+
}
|
|
242
|
+
const rows = rowsOf(value);
|
|
243
|
+
const relates = field.shape.fields.some((each) => each.kind !== 'scalar');
|
|
244
|
+
if (rows.length > field.size && relates) {
|
|
245
|
+
throw new RangeError(`${field.owner} returned ${rows.length} rows; a list counts take (${field.size}) rows, so it holds no more under a selected relation.`);
|
|
246
|
+
}
|
|
247
|
+
return rows.map((row) => shaped(row, field.shape));
|
|
248
|
+
}
|
|
249
|
+
/** A row as GraphQL returns it: its fields, its relations' pages. */
|
|
250
|
+
export function shaped(row, shape) {
|
|
251
|
+
const out = {};
|
|
252
|
+
for (const field of shape.fields) {
|
|
253
|
+
const value = row[field.name];
|
|
254
|
+
if (field.kind === 'scalar') {
|
|
255
|
+
out[field.name] =
|
|
256
|
+
value === undefined || value === null
|
|
257
|
+
? null
|
|
258
|
+
: field.toJSON === undefined
|
|
259
|
+
? value
|
|
260
|
+
: field.toJSON(value);
|
|
261
|
+
}
|
|
262
|
+
else if (field.kind === 'one') {
|
|
263
|
+
const target = rowOf(value);
|
|
264
|
+
out[field.name] =
|
|
265
|
+
target === undefined ? null : shaped(target, field.shape);
|
|
266
|
+
}
|
|
267
|
+
else if (field.kind === 'returned') {
|
|
268
|
+
out[field.name] = returned(field, value);
|
|
269
|
+
}
|
|
270
|
+
else if (value === undefined || value === null) {
|
|
271
|
+
// Absent when the field's read rule hides it (D-44).
|
|
272
|
+
out[field.name] = null;
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
const rows = rowsOf(value);
|
|
276
|
+
const node = (each) => shaped(each, field.shape);
|
|
277
|
+
if (field.relay === undefined) {
|
|
278
|
+
out[field.name] = rows.map(node);
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
const page = connection(rows, field.relay, node);
|
|
282
|
+
out[field.name] = field.counted
|
|
283
|
+
? { ...page, totalCount: countOf(row, field.name) }
|
|
284
|
+
: page;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return out;
|
|
288
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Routes } from '@ultimabase/http';
|
|
2
|
+
import type { Node } from '@ultimabase/http/internal';
|
|
3
|
+
import type { ExposedFunction } from 'ultimabase';
|
|
4
|
+
import type { Api } from './entries.ts';
|
|
5
|
+
import type { Accepted } from './params.ts';
|
|
6
|
+
import type { Live } from './streams.ts';
|
|
7
|
+
/** A function's key of `.with` (D-129): its route's name, or hidden. */
|
|
8
|
+
export type RestFunctionWith = {
|
|
9
|
+
readonly name?: string;
|
|
10
|
+
} | false;
|
|
11
|
+
/** A function REST routes, named, and its input's JSON Schema. */
|
|
12
|
+
export type Routed = {
|
|
13
|
+
readonly fn: ExposedFunction<RestFunctionWith>;
|
|
14
|
+
readonly owner: string;
|
|
15
|
+
readonly name: string;
|
|
16
|
+
readonly input: Node | undefined;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* A GET's parameters: an object input's keys, as GraphQL's arguments, or
|
|
20
|
+
* one `input`; a string as is, any other value as JSON.
|
|
21
|
+
*/
|
|
22
|
+
export declare function getParams(root: Node | undefined): {
|
|
23
|
+
readonly accepted: Accepted;
|
|
24
|
+
readonly flat: boolean;
|
|
25
|
+
};
|
|
26
|
+
/** Each routed function under `${base}/functions` (D-109, D-113). */
|
|
27
|
+
export declare function functionRoutes(app: Routes, api: Api, functions: readonly ExposedFunction<RestFunctionWith>[], base: string, live: Live | undefined): readonly Routed[];
|
|
28
|
+
//# sourceMappingURL=functions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"functions.d.ts","sourceRoot":"","sources":["../src/functions.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAE9C,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,2BAA2B,CAAA;AAErD,OAAO,KAAK,EAAkB,eAAe,EAAiB,MAAM,YAAY,CAAA;AAEhF,OAAO,KAAK,EAAE,GAAG,EAAS,MAAM,cAAc,CAAA;AAK9C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAG3C,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAA;AAExC,wEAAwE;AACxE,MAAM,MAAM,gBAAgB,GAAG;IAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,KAAK,CAAA;AAIjE,kEAAkE;AAClE,MAAM,MAAM,MAAM,GAAG;IACpB,QAAQ,CAAC,EAAE,EAAE,eAAe,CAAC,gBAAgB,CAAC,CAAA;IAC9C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,KAAK,EAAE,IAAI,GAAG,SAAS,CAAA;CAChC,CAAA;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,IAAI,GAAG,SAAS,GAAG;IAClD,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAA;IAC3B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;CACtB,CAiBA;AAwED,qEAAqE;AACrE,wBAAgB,cAAc,CAC7B,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,GAAG,EACR,SAAS,EAAE,SAAS,eAAe,CAAC,gBAAgB,CAAC,EAAE,EACvD,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,IAAI,GAAG,SAAS,GACpB,SAAS,MAAM,EAAE,CAoEnB"}
|