@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.
Files changed (99) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +4 -2
  3. package/dist/data-types.d.ts +15 -0
  4. package/dist/data-types.d.ts.map +1 -0
  5. package/dist/data-types.js +53 -0
  6. package/dist/data.d.ts +34 -0
  7. package/dist/data.d.ts.map +1 -0
  8. package/dist/data.js +207 -0
  9. package/dist/depth.d.ts +6 -0
  10. package/dist/depth.d.ts.map +1 -0
  11. package/dist/depth.js +67 -0
  12. package/dist/errors.d.ts +7 -0
  13. package/dist/errors.d.ts.map +1 -0
  14. package/dist/errors.js +42 -0
  15. package/dist/functions.d.ts +28 -0
  16. package/dist/functions.d.ts.map +1 -0
  17. package/dist/functions.js +195 -0
  18. package/dist/graphiql.d.ts +3 -0
  19. package/dist/graphiql.d.ts.map +1 -0
  20. package/dist/graphiql.js +46 -0
  21. package/dist/index.d.ts +47 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +165 -0
  24. package/dist/inputs.d.ts +20 -0
  25. package/dist/inputs.d.ts.map +1 -0
  26. package/dist/inputs.js +236 -0
  27. package/dist/json-fields.d.ts +12 -0
  28. package/dist/json-fields.d.ts.map +1 -0
  29. package/dist/json-fields.js +66 -0
  30. package/dist/json-schema.d.ts +7 -0
  31. package/dist/json-schema.d.ts.map +1 -0
  32. package/dist/json-schema.js +16 -0
  33. package/dist/live-stream.d.ts +59 -0
  34. package/dist/live-stream.d.ts.map +1 -0
  35. package/dist/live-stream.js +136 -0
  36. package/dist/mutations.d.ts +9 -0
  37. package/dist/mutations.d.ts.map +1 -0
  38. package/dist/mutations.js +228 -0
  39. package/dist/names.d.ts +8 -0
  40. package/dist/names.d.ts.map +1 -0
  41. package/dist/names.js +14 -0
  42. package/dist/outputs.d.ts +16 -0
  43. package/dist/outputs.d.ts.map +1 -0
  44. package/dist/outputs.js +177 -0
  45. package/dist/overlap.d.ts +5 -0
  46. package/dist/overlap.d.ts.map +1 -0
  47. package/dist/overlap.js +82 -0
  48. package/dist/pages.d.ts +11 -0
  49. package/dist/pages.d.ts.map +1 -0
  50. package/dist/pages.js +22 -0
  51. package/dist/registry.d.ts +80 -0
  52. package/dist/registry.d.ts.map +1 -0
  53. package/dist/registry.js +97 -0
  54. package/dist/reread.d.ts +9 -0
  55. package/dist/reread.d.ts.map +1 -0
  56. package/dist/reread.js +116 -0
  57. package/dist/rows.d.ts +23 -0
  58. package/dist/rows.d.ts.map +1 -0
  59. package/dist/rows.js +106 -0
  60. package/dist/scalars.d.ts +13 -0
  61. package/dist/scalars.d.ts.map +1 -0
  62. package/dist/scalars.js +88 -0
  63. package/dist/schema.d.ts +17 -0
  64. package/dist/schema.d.ts.map +1 -0
  65. package/dist/schema.js +130 -0
  66. package/dist/select.d.ts +24 -0
  67. package/dist/select.d.ts.map +1 -0
  68. package/dist/select.js +152 -0
  69. package/dist/sockets.d.ts +25 -0
  70. package/dist/sockets.d.ts.map +1 -0
  71. package/dist/sockets.js +234 -0
  72. package/dist/subscriptions.d.ts +14 -0
  73. package/dist/subscriptions.d.ts.map +1 -0
  74. package/dist/subscriptions.js +242 -0
  75. package/package.json +53 -3
  76. package/src/data-types.ts +100 -0
  77. package/src/data.ts +366 -0
  78. package/src/depth.ts +98 -0
  79. package/src/errors.ts +48 -0
  80. package/src/functions.ts +291 -0
  81. package/src/graphiql.ts +54 -0
  82. package/src/index.ts +231 -0
  83. package/src/inputs.ts +348 -0
  84. package/src/json-fields.ts +95 -0
  85. package/src/json-schema.ts +21 -0
  86. package/src/live-stream.ts +192 -0
  87. package/src/mutations.ts +317 -0
  88. package/src/names.ts +25 -0
  89. package/src/outputs.ts +267 -0
  90. package/src/overlap.ts +97 -0
  91. package/src/pages.ts +39 -0
  92. package/src/registry.ts +189 -0
  93. package/src/reread.ts +159 -0
  94. package/src/rows.ts +151 -0
  95. package/src/scalars.ts +110 -0
  96. package/src/schema.ts +189 -0
  97. package/src/select.ts +230 -0
  98. package/src/sockets.ts +291 -0
  99. package/src/subscriptions.ts +334 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACX,qBAAqB,EACrB,sBAAsB,EACtB,iBAAiB,EACjB,kBAAkB,EAClB,iBAAiB,EACjB,MAAM,SAAS,CAAA;AAChB,OAAO,EAEN,eAAe,EAGf,WAAW,EACX,cAAc,EAEd,MAAM,SAAS,CAAA;AAChB,OAAO,KAAK,EACX,MAAM,EACN,MAAM,EACN,YAAY,EACZ,WAAW,EACX,WAAW,EACX,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAI3C,wCAAwC;AACxC,MAAM,MAAM,WAAW,GACtB;IAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,KAAK,CAAA;AAEhE,gFAAgF;AAChF,MAAM,MAAM,OAAO,GAAG;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,uEAAuE;IACvE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,kBAAkB,KAAK;QAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAA;KAAE,CAAA;CACpE,CAAA;AACD,qEAAqE;AACrE,eAAO,MAAM,EAAE,YAAa,OAAO,KAAG;IAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CAEzD,CAAA;AACF,MAAM,MAAM,IAAI,GAAG;IAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,CAAA;AACtD,MAAM,MAAM,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAC,CAAA;AAEtE,MAAM,MAAM,KAAK,GAAG;IACnB,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC,WAAW,CAAC,CAAA;IAC3C,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAA;IAC3B,uEAAuE;IACvE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,iEAAiE;IACjE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACxB,CAAA;AAED,MAAM,MAAM,QAAQ,GAAG;IACtB,QAAQ,CAAC,UAAU,EAAE,YAAY,GAAG,MAAM,CAAA;CAC1C,CAAA;AAUD,qBAAa,QAAQ;;IAIpB,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,KAAK,CAAC,CAAA;IAC5C,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAA;IAC3B,wEAAwE;IACxE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;IACxB,4EAA4E;IAC5E,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,qBAAqB,CAAC,OAAO,EAAE,OAAO,CAAC,CAC9D;IAEX,YAAY,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAWlE;IAED,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,CAI1B;IAED,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,iBAAiB,GAAG,iBAAiB,CAEtE;IAED,KAAK,CACJ,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,sBAAsB,GACjC,sBAAsB,CAExB;IAED,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,eAAe,GAAG,eAAe,CAEhE;CACD;AAED,+EAA+E;AAC/E,wBAAgB,SAAS,CACxB,CAAC,SAAS;IAAE,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;CAAE,EAC7D,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE;IAAE,CAAC,KAAK,EAAE,MAAM,GAAG,CAAC,CAAA;CAAE,GAAG;IAAE,CAAC,KAAK,EAAE,MAAM,GAAG,CAAC,CAAA;CAAE,CAQtE;AAED,eAAO,MAAM,WAAW,EAAE;IACzB,QAAQ,EAAE,CAAC,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,GAAG,iBAAiB;CASlE,CAAA;AAED;;;GAGG;AACH,wBAAgB,UAAU,CACzB,QAAQ,EAAE,QAAQ,EAClB,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,GACX,iBAAiB,GAAG,eAAe,CAmBrC;AAED,+EAA+E;AAC/E,eAAO,MAAM,SAAS,aACX,QAAQ,SACX,MAAM,SACN,MAAM,KAEX,iBAAiB,GACjB,eAAe,GACf,WAAW,CAAC,cAAc,CAAC,iBAAiB,GAAG,eAAe,CAAC,CAGjE,CAAA"}
@@ -0,0 +1,97 @@
1
+ import { GraphQLBoolean, GraphQLEnumType, GraphQLFloat, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLString, } from 'graphql';
2
+ import { DataTypes } from './data-types.js';
3
+ import { upper } from './names.js';
4
+ import { DateScalar, DateTime, DecimalScalar, JSONScalar } from './scalars.js';
5
+ /** A kernel query's options: run as the request's caller (D-106). */
6
+ export const as = (context) => ({
7
+ as: context.caller,
8
+ });
9
+ function memo(cache, name, build) {
10
+ const found = cache.get(name);
11
+ if (found !== undefined)
12
+ return found;
13
+ const built = build();
14
+ cache.set(name, built);
15
+ return built;
16
+ }
17
+ export class Registry {
18
+ #objects = new Map();
19
+ #inputs = new Map();
20
+ #enums = new Map();
21
+ entries;
22
+ settings;
23
+ /** The types functions and json fields give, shared by both (D-125). */
24
+ data;
25
+ /** A model's virtual fields, which the functions' outputs build (D-151). */
26
+ virtualFields = () => ({});
27
+ constructor(entries, settings) {
28
+ this.entries = entries;
29
+ this.settings = settings;
30
+ this.data = new DataTypes(entries.keys());
31
+ // Before any function's: a `defineEnum` keeps its own description.
32
+ for (const [model, { exposed }] of entries) {
33
+ for (const [field, shared] of Object.entries(exposed.enums)) {
34
+ const { name, values, description } = shared;
35
+ this.data.enum(name, values, `${model}.${field}`, description);
36
+ }
37
+ }
38
+ }
39
+ entry(model) {
40
+ const entry = this.entries.get(model);
41
+ if (entry === undefined)
42
+ throw new Error(`Unknown model ${model}.`);
43
+ return entry;
44
+ }
45
+ object(name, build) {
46
+ return memo(this.#objects, name, build);
47
+ }
48
+ input(name, build) {
49
+ return memo(this.#inputs, name, build);
50
+ }
51
+ enum(name, build) {
52
+ return memo(this.#enums, name, build);
53
+ }
54
+ }
55
+ /** Each field's `description` (D-150), on the fields of a type or an input. */
56
+ export function described(entry, fields) {
57
+ for (const [field, description] of Object.entries(entry.exposed.descriptions)) {
58
+ const config = fields[field];
59
+ if (config !== undefined)
60
+ fields[field] = { ...config, description };
61
+ }
62
+ return fields;
63
+ }
64
+ export const scalarTypes = {
65
+ string: GraphQLString,
66
+ number: GraphQLFloat,
67
+ decimal: DecimalScalar,
68
+ boolean: GraphQLBoolean,
69
+ dateTime: DateTime,
70
+ date: DateScalar,
71
+ json: JSONScalar,
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 function scalarType(registry, model, field) {
78
+ if (field.integer === true)
79
+ return GraphQLInt;
80
+ if (field.type !== 'enum')
81
+ return scalarTypes[field.type];
82
+ const shared = registry.entry(model).exposed.enums[field.name];
83
+ if (shared !== undefined) {
84
+ const { name, values } = shared;
85
+ return registry.data.enum(name, values, `${model}.${field.name}`);
86
+ }
87
+ const name = `${model}${upper(field.name)}`;
88
+ return registry.enum(name, () => new GraphQLEnumType({
89
+ name,
90
+ values: Object.fromEntries((field.values ?? []).map((value) => [value, { value }])),
91
+ }));
92
+ }
93
+ /** A scalar field's value, read or written: a list's is `[Float!]` (D-127). */
94
+ export const valueType = (registry, model, field) => {
95
+ const item = scalarType(registry, model, field);
96
+ return field.list ? new GraphQLList(new GraphQLNonNull(item)) : item;
97
+ };
@@ -0,0 +1,9 @@
1
+ import type { GraphQLResolveInfo } from 'graphql';
2
+ import type { ExposedOutput } from 'ultimabase';
3
+ import type { Context, Registry } from './registry.ts';
4
+ /** `run`'s value, its rows' relations read as the caller. */
5
+ export declare function reread(registry: Registry, output: ExposedOutput, value: unknown, context: Context, info: GraphQLResolveInfo, call: {
6
+ readonly name: string;
7
+ readonly take: number;
8
+ }): Promise<unknown>;
9
+ //# sourceMappingURL=reread.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reread.d.ts","sourceRoot":"","sources":["../src/reread.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAa,kBAAkB,EAAE,MAAM,SAAS,CAAA;AAC5D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE/C,OAAO,KAAK,EAAQ,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAgB5D,6DAA6D;AAC7D,wBAAsB,MAAM,CAC3B,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,aAAa,EACrB,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,kBAAkB,EACxB,IAAI,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACpD,OAAO,CAAC,OAAO,CAAC,CA+HlB"}
package/dist/reread.js ADDED
@@ -0,0 +1,116 @@
1
+ // A function's output as the client reads it (D-109): its scalars as `run`
2
+ // returned them, its rows' relations re-read by id as the caller, in one
3
+ // query per model and only when a relation is selected.
4
+ import { badInput, rowsOf, run } from '@ultimabase/http/internal';
5
+ import { isMaster, objectType } from './outputs.js';
6
+ import { as } from './registry.js';
7
+ import { grouped, selectOf } from './select.js';
8
+ const isRecord = (value) => typeof value === 'object' && value !== null;
9
+ /** `run`'s value, its rows' relations read as the caller. */
10
+ export async function reread(registry, output, value, context, info, call) {
11
+ const master = isMaster(context);
12
+ // The rows whose relations are read, which the row budget counts.
13
+ let counted = 0;
14
+ const pending = new Map();
15
+ // A list's items share their nodes: each selection is read once.
16
+ const relations = new Map();
17
+ const fields = new Map();
18
+ const relationsOf = (model, nodes) => {
19
+ const known = relations.get(nodes);
20
+ if (known !== undefined)
21
+ return known;
22
+ const type = objectType(registry, model);
23
+ const select = selectOf(registry, info, type, nodes, master);
24
+ // A virtual is computed when its row is read (D-151).
25
+ const { virtuals } = registry.entry(model).exposed;
26
+ const found = Object.fromEntries(Object.entries(select).filter(([name, read]) => read !== true || virtuals.some((virtual) => virtual.name === name)));
27
+ relations.set(nodes, found);
28
+ return found;
29
+ };
30
+ const fieldsOf = (nodes) => {
31
+ const known = fields.get(nodes);
32
+ if (known !== undefined)
33
+ return known;
34
+ const found = grouped(info, nodes);
35
+ fields.set(nodes, found);
36
+ return found;
37
+ };
38
+ const row = (model, item, nodes) => {
39
+ const select = relationsOf(model, nodes);
40
+ if (Object.keys(select).length === 0)
41
+ return item;
42
+ const copy = { ...item };
43
+ const { id } = copy;
44
+ if (typeof id !== 'string') {
45
+ throw new TypeError(`A ${model} was returned without its id.`);
46
+ }
47
+ const reading = pending.get(model) ?? {
48
+ select: {},
49
+ rows: new Map(),
50
+ };
51
+ pending.set(model, reading);
52
+ for (const [name, read] of Object.entries(select)) {
53
+ const known = reading.select[name];
54
+ if (known !== undefined &&
55
+ JSON.stringify(known) !== JSON.stringify(read)) {
56
+ throw badInput(`${model}.${name} is selected in two ways; select it the same way everywhere.`);
57
+ }
58
+ reading.select[name] = read;
59
+ }
60
+ reading.rows.set(id, [...(reading.rows.get(id) ?? []), copy]);
61
+ counted += 1;
62
+ return copy;
63
+ };
64
+ const one = (named, item, nodes) => {
65
+ if (!isRecord(item))
66
+ return item;
67
+ if (named.kind === 'model')
68
+ return row(named.name, item, nodes);
69
+ const copy = { ...item };
70
+ const selected = fieldsOf(nodes);
71
+ for (const [name, field] of named.type.fields) {
72
+ const below = selected.get(name);
73
+ if (below !== undefined) {
74
+ copy[name] = walk(field, Reflect.get(item, name), below);
75
+ }
76
+ }
77
+ return copy;
78
+ };
79
+ const walk = (at, item, nodes) => {
80
+ if (at.kind === 'schema')
81
+ return item;
82
+ if (at.form !== 'list' || !Array.isArray(item)) {
83
+ return one(at, item, nodes);
84
+ }
85
+ const before = counted;
86
+ const items = item.map((each) => one(at, each, nodes));
87
+ // The budget counts `take` rows in a list (D-109).
88
+ if (items.length > call.take && counted > before) {
89
+ throw new RangeError(`${call.name} returned ${items.length} rows; a list counts take (${call.take}) rows, so it holds no more under a selected relation.`);
90
+ }
91
+ return items;
92
+ };
93
+ const result = walk(output, value, info.fieldNodes);
94
+ for (const [model, { select, rows }] of pending) {
95
+ const entry = registry.entry(model);
96
+ const ids = [...rows.keys()];
97
+ const read = await run(() => entry.exposed.queries.findMany({
98
+ where: { id: { in: ids } },
99
+ take: ids.length,
100
+ select: { id: true, ...select },
101
+ }, as(context)));
102
+ const byId = new Map(rowsOf(read).map((found) => [found.id, found]));
103
+ for (const [id, copies] of rows) {
104
+ const found = byId.get(id);
105
+ for (const name of Object.keys(select)) {
106
+ // A row the caller cannot read has no relations (D-109).
107
+ const many = entry.model.fields.some((field) => field.name === name && field.kind === 'many');
108
+ const empty = many ? [] : null;
109
+ const relation = found === undefined ? empty : Reflect.get(found, name);
110
+ for (const copy of copies)
111
+ copy[name] = relation;
112
+ }
113
+ }
114
+ }
115
+ return result;
116
+ }
package/dist/rows.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ import type { ValidatedExecutionArgs } from 'graphql';
2
+ import { GraphQLError } from 'graphql';
3
+ import type { Entry, Registry } from './registry.ts';
4
+ /**
5
+ * What a field reads: one row, a list of a model's rows, or a function's
6
+ * list, which counts `take` rows (D-109).
7
+ */
8
+ export type Read = {
9
+ readonly kind: 'one';
10
+ } | {
11
+ readonly kind: 'many';
12
+ readonly entry: Entry;
13
+ } | {
14
+ readonly kind: 'list';
15
+ readonly size: number;
16
+ };
17
+ export type Limits = {
18
+ readonly maxRows: number;
19
+ readonly maxDepth: number;
20
+ };
21
+ /** The refusal of a document that may return more than `maxRows` rows. */
22
+ export declare function rowBudget(registry: Registry, { maxRows, maxDepth }: Limits, functions: ReadonlyMap<string, ReadonlyMap<string, Read>>): (args: ValidatedExecutionArgs) => GraphQLError | undefined;
23
+ //# sourceMappingURL=rows.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rows.d.ts","sourceRoot":"","sources":["../src/rows.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAGX,sBAAsB,EACtB,MAAM,SAAS,CAAA;AAChB,OAAO,EAGN,YAAY,EAEZ,MAAM,SAAS,CAAA;AAGhB,OAAO,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAEpD;;;GAGG;AACH,MAAM,MAAM,IAAI,GACb;IAAE,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAA;CAAE,GACxB;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;CAAE,GAChD;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAA;AAoCnD,MAAM,MAAM,MAAM,GAAG;IAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAA;AAE5E,0EAA0E;AAC1E,wBAAgB,SAAS,CACxB,QAAQ,EAAE,QAAQ,EAClB,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,MAAM,EAC7B,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,GACvD,CAAC,IAAI,EAAE,sBAAsB,KAAK,YAAY,GAAG,SAAS,CAgF5D"}
package/dist/rows.js ADDED
@@ -0,0 +1,106 @@
1
+ import { getArgumentValues, getNamedType, GraphQLError, isObjectType, } from 'graphql';
2
+ import { grouped } from './select.js';
3
+ const one = { kind: 'one' };
4
+ /** Each a query or a write of its own, a count one more. */
5
+ const maxRoots = 20;
6
+ const responseKey = (node) => node.alias?.value ?? node.name.value;
7
+ // `take` for none or a negative number; above `maxTake`, the list refuses.
8
+ const sizeOf = (entry, asked) => typeof asked === 'number' && asked >= 0
9
+ ? Math.min(asked, entry.maxTake)
10
+ : entry.take;
11
+ /** The reads of the root query, by field name. */
12
+ function rootReads(registry) {
13
+ const reads = new Map();
14
+ for (const entry of registry.entries.values()) {
15
+ if (!entry.visible)
16
+ continue;
17
+ reads.set(entry.single, one);
18
+ reads.set(entry.list, { kind: 'many', entry });
19
+ }
20
+ return reads;
21
+ }
22
+ /** The relations an `orderBy` goes through, the deepest path's. */
23
+ function sortDepth(orderBy) {
24
+ if (Array.isArray(orderBy))
25
+ return Math.max(0, ...orderBy.map(sortDepth));
26
+ if (typeof orderBy !== 'object' || orderBy === null)
27
+ return 0;
28
+ return Math.max(0, ...Object.values(orderBy).map((value) => typeof value === 'object' && value !== null ? 1 + sortDepth(value) : 0));
29
+ }
30
+ /** The refusal of a document that may return more than `maxRows` rows. */
31
+ export function rowBudget(registry, { maxRows, maxDepth }, functions) {
32
+ const roots = rootReads(registry);
33
+ const list = registry.settings.pagination === 'list';
34
+ const readOf = (type, root, name) => {
35
+ const called = functions.get(type.name);
36
+ if (called?.has(name) === true)
37
+ return called.get(name);
38
+ if (root === 'mutation')
39
+ return one;
40
+ if (root === 'query')
41
+ return roots.get(name);
42
+ const entry = registry.entries.get(type.name);
43
+ const field = entry?.model.fields.find((f) => f.name === name);
44
+ if (field === undefined || field.kind === 'scalar')
45
+ return undefined;
46
+ if (field.kind === 'one')
47
+ return one;
48
+ return { kind: 'many', entry: registry.entry(field.target) };
49
+ };
50
+ return (args) => {
51
+ const info = {
52
+ fragments: args.fragmentDefinitions,
53
+ variableValues: args.variableValues,
54
+ };
55
+ let rows = 0;
56
+ let rootFields = 0;
57
+ let deepest = 0;
58
+ const walk = (type, nodes, times, root, level) => {
59
+ for (const fields of grouped(info, nodes, responseKey).values()) {
60
+ if (rows > maxRows)
61
+ return;
62
+ const [node] = fields;
63
+ const field = type.getFields()[node.name.value];
64
+ if (field === undefined)
65
+ continue;
66
+ const read = readOf(type, root, node.name.value);
67
+ let each = times;
68
+ if (read?.kind === 'many') {
69
+ const asked = getArgumentValues(field, node, args.variableValues);
70
+ const size = sizeOf(read.entry, list ? asked['take'] : (asked['last'] ?? asked['first']));
71
+ // A count reads the rows it counts, however few the page returns.
72
+ deepest = Math.max(deepest, level + sortDepth(asked['orderBy']));
73
+ const counted = grouped(info, fields).has('totalCount');
74
+ each *= counted ? Math.max(size, read.entry.take) : size;
75
+ if (counted && root !== '')
76
+ rootFields += 1;
77
+ }
78
+ else if (read?.kind === 'list')
79
+ each *= read.size;
80
+ if (read !== undefined)
81
+ rows += each;
82
+ if (root !== '')
83
+ rootFields += 1;
84
+ const named = getNamedType(field.type);
85
+ if (isObjectType(named) && each > 0) {
86
+ walk(named, fields, each, '', level + 1);
87
+ }
88
+ }
89
+ };
90
+ const { operation } = args;
91
+ const type = args.schema.getRootType(operation.operation);
92
+ // `validateExecutionArgs` refused an operation the schema has no root for.
93
+ if (type)
94
+ walk(type, [operation], 1, operation.operation, 1);
95
+ if (deepest > maxDepth) {
96
+ return new GraphQLError(`The document is nested ${deepest} levels deep, its orderBy included; the most is ${maxDepth}.`);
97
+ }
98
+ if (rootFields > maxRoots) {
99
+ return new GraphQLError(`The document asks for more than ${maxRoots} root fields, a totalCount counted as one more. Send several documents.`);
100
+ }
101
+ if (rows <= maxRows)
102
+ return undefined;
103
+ const fewer = list ? 'take' : 'first or last';
104
+ return new GraphQLError(`The document asks for more than ${maxRows.toLocaleString('en-US')} rows. Ask for fewer with ${fewer}.`);
105
+ };
106
+ }
@@ -0,0 +1,13 @@
1
+ import { GraphQLScalarType } from 'graphql';
2
+ /** A date a client sent, which keeps its text for a function (D-109). */
3
+ export declare class SentDate extends Date {
4
+ readonly sent: string;
5
+ constructor(sent: string);
6
+ }
7
+ export declare const DateTime: GraphQLScalarType<Date, string>;
8
+ export declare const DecimalScalar: GraphQLScalarType<string, string>;
9
+ export declare const DateScalar: GraphQLScalarType<string, string>;
10
+ /** Dates back to their text: a validator reads what the client sent. */
11
+ export declare function plain(value: unknown): unknown;
12
+ export declare const JSONScalar: GraphQLScalarType<unknown, unknown>;
13
+ //# sourceMappingURL=scalars.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scalars.d.ts","sourceRoot":"","sources":["../src/scalars.ts"],"names":[],"mappings":"AAIA,OAAO,EAAgB,iBAAiB,EAAQ,MAAM,SAAS,CAAA;AAK/D,yEAAyE;AACzE,qBAAa,QAAS,SAAQ,IAAI;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IAErB,YAAY,IAAI,EAAE,MAAM,EAGvB;CACD;AAYD,eAAO,MAAM,QAAQ,EAAE,iBAAiB,CAAC,IAAI,EAAE,MAAM,CAenD,CAAA;AAOF,eAAO,MAAM,aAAa,EAAE,iBAAiB,CAAC,MAAM,EAAE,MAAM,CAQzD,CAAA;AAOH,eAAO,MAAM,UAAU,EAAE,iBAAiB,CAAC,MAAM,EAAE,MAAM,CAQtD,CAAA;AAEH,wEAAwE;AACxE,wBAAgB,KAAK,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAO7C;AAkBD,eAAO,MAAM,UAAU,EAAE,iBAAiB,CAAC,OAAO,EAAE,OAAO,CAOxD,CAAA"}
@@ -0,0 +1,88 @@
1
+ import { GraphQLError, GraphQLScalarType, Kind } from 'graphql';
2
+ const iso = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})$/;
3
+ /** A date a client sent, which keeps its text for a function (D-109). */
4
+ export class SentDate extends Date {
5
+ sent;
6
+ constructor(sent) {
7
+ super(sent);
8
+ this.sent = sent;
9
+ }
10
+ }
11
+ function date(value) {
12
+ if (typeof value === 'string' && iso.test(value)) {
13
+ const parsed = new SentDate(value);
14
+ if (!Number.isNaN(parsed.getTime()))
15
+ return parsed;
16
+ }
17
+ throw new GraphQLError('A DateTime is an ISO 8601 string, such as 2026-01-31T12:00:00.000Z.');
18
+ }
19
+ export const DateTime = new GraphQLScalarType({
20
+ name: 'DateTime',
21
+ description: 'A date and time, as an ISO 8601 string.',
22
+ coerceOutputValue: (value) => {
23
+ if (value instanceof Date)
24
+ return value.toISOString();
25
+ // A function's schema output holds its dates as strings (D-109).
26
+ if (typeof value === 'string')
27
+ return date(value).toISOString();
28
+ throw new GraphQLError('A DateTime field returned no date.');
29
+ },
30
+ coerceInputValue: date,
31
+ coerceInputLiteral: (node) => date(node.kind === Kind.STRING ? node.value : undefined),
32
+ });
33
+ function decimal(value) {
34
+ if (typeof value === 'string')
35
+ return value;
36
+ throw new GraphQLError("A Decimal is a string, such as '12.50'.");
37
+ }
38
+ export const DecimalScalar = new GraphQLScalarType({
39
+ name: 'Decimal',
40
+ description: "An exact number, as a string such as '12.50'.",
41
+ coerceOutputValue: decimal,
42
+ coerceInputValue: decimal,
43
+ coerceInputLiteral: (node) => decimal(node.kind === Kind.STRING ? node.value : undefined),
44
+ });
45
+ function day(value) {
46
+ if (typeof value === 'string')
47
+ return value;
48
+ throw new GraphQLError("A Date is a day as a string, such as '2026-07-31'.");
49
+ }
50
+ export const DateScalar = new GraphQLScalarType({
51
+ name: 'Date',
52
+ description: "A day, as an ISO 8601 string such as '2026-07-31'.",
53
+ coerceOutputValue: day,
54
+ coerceInputValue: day,
55
+ coerceInputLiteral: (node) => day(node.kind === Kind.STRING ? node.value : undefined),
56
+ });
57
+ /** Dates back to their text: a validator reads what the client sent. */
58
+ export function plain(value) {
59
+ if (value instanceof SentDate)
60
+ return value.sent;
61
+ if (Array.isArray(value))
62
+ return value.map(plain);
63
+ if (typeof value !== 'object' || value === null)
64
+ return value;
65
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, plain(item)]));
66
+ }
67
+ /** A literal as the plain value the kernel's JSON check accepts. */
68
+ function literal(node) {
69
+ if (node.kind === Kind.OBJECT) {
70
+ return Object.fromEntries(node.fields.map((field) => [field.name.value, literal(field.value)]));
71
+ }
72
+ if (node.kind === Kind.LIST)
73
+ return node.values.map(literal);
74
+ if (node.kind === Kind.INT || node.kind === Kind.FLOAT) {
75
+ return Number(node.value);
76
+ }
77
+ if (node.kind === Kind.NULL)
78
+ return null;
79
+ // A string, a boolean or an enum's name.
80
+ return node.value;
81
+ }
82
+ export const JSONScalar = new GraphQLScalarType({
83
+ name: 'JSON',
84
+ description: 'Any JSON value.',
85
+ coerceOutputValue: (value) => value,
86
+ coerceInputValue: (value) => value,
87
+ coerceInputLiteral: literal,
88
+ });
@@ -0,0 +1,17 @@
1
+ import type { GraphQLFieldConfig, GraphQLNamedType } from 'graphql';
2
+ import { GraphQLSchema } from 'graphql';
3
+ import type { Args, Context, Registry } from './registry.ts';
4
+ import type { RootField } from './functions.ts';
5
+ type Fields = {
6
+ [field: string]: GraphQLFieldConfig<unknown, Context, Args>;
7
+ };
8
+ /** Refuses two root fields of one name, naming both owners. */
9
+ export declare function schemaOf(registry: Registry, called: {
10
+ readonly fields: readonly RootField[];
11
+ readonly owners: ReadonlyMap<string, string>;
12
+ readonly named: (name: string) => GraphQLNamedType | undefined;
13
+ /** The live models' subscriptions, named as their queries (D-154). */
14
+ readonly subscriptions: Fields;
15
+ }): GraphQLSchema;
16
+ export {};
17
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AACnE,OAAO,EAIN,aAAa,EACb,MAAM,SAAS,CAAA;AAIhB,OAAO,KAAK,EAAE,IAAI,EAAE,OAAO,EAAS,QAAQ,EAAE,MAAM,eAAe,CAAA;AAInE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAG/C,KAAK,MAAM,GAAG;IAAE,CAAC,KAAK,EAAE,MAAM,GAAG,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,CAAA;CAAE,CAAA;AAwF7E,+DAA+D;AAC/D,wBAAgB,QAAQ,CACvB,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE;IACP,QAAQ,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,CAAA;IACrC,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC5C,QAAQ,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,gBAAgB,GAAG,SAAS,CAAA;IAC9D,sEAAsE;IACtE,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;CAC9B,GACC,aAAa,CAsEf"}
package/dist/schema.js ADDED
@@ -0,0 +1,130 @@
1
+ // The schema (D-107): for each visible model, `post(where)` and `posts(...)`,
2
+ // each read in one kernel query as its caller, and its mutations.
3
+ import { connection, rowsOf, run } from '@ultimabase/http/internal';
4
+ import { GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLSchema, } from 'graphql';
5
+ import { objectOf, uniqueInput, uniqueWhere } from './inputs.js';
6
+ import { connectionType, isMaster, listArgs, objectType } from './outputs.js';
7
+ import { listRead, whereOf } from './pages.js';
8
+ import { as } from './registry.js';
9
+ import { rowNodes, selectOf } from './select.js';
10
+ import { mutationFields } from './mutations.js';
11
+ import { Refused } from 'ultimabase';
12
+ function rootFields(registry, entry) {
13
+ const name = entry.model.name;
14
+ const type = objectType(registry, name);
15
+ const { queries } = entry.exposed;
16
+ const list = registry.settings.pagination === 'list'
17
+ ? new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(type)))
18
+ : new GraphQLNonNull(connectionType(registry, `${name}Connection`, name));
19
+ return {
20
+ [entry.single]: {
21
+ type,
22
+ args: {
23
+ where: { type: new GraphQLNonNull(uniqueInput(registry, name)) },
24
+ },
25
+ resolve: async (_, args, context, info) => {
26
+ const where = uniqueWhere(registry, name, objectOf(args['where']));
27
+ const select = selectOf(registry, info, type, info.fieldNodes, isMaster(context));
28
+ return run(() => queries.findUnique({ where, select }, as(context)));
29
+ },
30
+ },
31
+ [entry.list]: {
32
+ type: list,
33
+ args: listArgs(registry, name),
34
+ resolve: async (_, args, context, info) => {
35
+ const master = isMaster(context);
36
+ const { read, page } = listRead(registry, entry, args, master);
37
+ const select = selectOf(registry, info, type, rowNodes(registry, info), master);
38
+ const rows = await run(() => queries.findMany({ ...read, select }, as(context)));
39
+ if (page === undefined)
40
+ return rows;
41
+ let counted;
42
+ return {
43
+ ...connection(rowsOf(rows), page, (row) => row),
44
+ // Called by the default resolver, so counted only when selected,
45
+ // and once however many aliases ask.
46
+ totalCount: async () => {
47
+ counted ??= run(() => queries.count(whereOf(registry, name, args), as(context)));
48
+ return counted;
49
+ },
50
+ };
51
+ },
52
+ },
53
+ };
54
+ }
55
+ const taken = (name, owner) => new Refused(`${owner}: the type ${name} is a generated type's name.`);
56
+ /** The models' types, a json field's among them, built once to name them. */
57
+ function generatedTypes(fields, mutations, typeOwners) {
58
+ try {
59
+ return new GraphQLSchema({
60
+ query: new GraphQLObjectType({ name: 'Query', fields: { ...fields } }),
61
+ mutation: new GraphQLObjectType({
62
+ name: 'Mutation',
63
+ fields: { ...mutations },
64
+ }),
65
+ }).getTypeMap();
66
+ }
67
+ catch (error) {
68
+ // A json field's type named like a model's, as `PostStatus`.
69
+ const name = /multiple types named "(\w+)"/.exec(String(error))?.[1];
70
+ const owner = name === undefined ? undefined : typeOwners.get(name);
71
+ if (name === undefined || owner === undefined)
72
+ throw error;
73
+ throw taken(name, owner);
74
+ }
75
+ }
76
+ /** Refuses two root fields of one name, naming both owners. */
77
+ export function schemaOf(registry, called) {
78
+ const { fields: functions, owners: typeOwners, named, subscriptions } = called;
79
+ const roots = {
80
+ query: new Map(),
81
+ mutation: new Map(),
82
+ };
83
+ const fields = {};
84
+ const mutations = {};
85
+ const add = (into, owners, owner, hint, added) => {
86
+ for (const [name, field] of Object.entries(added)) {
87
+ const other = owners.get(name);
88
+ if (other !== undefined) {
89
+ throw new Refused(`${owner}: the root field ${name} is ${other}'s; set .with({ graphql: { ${hint} } }).`);
90
+ }
91
+ owners.set(name, owner);
92
+ into[name] = field;
93
+ }
94
+ };
95
+ for (const entry of registry.entries.values()) {
96
+ if (!entry.visible)
97
+ continue;
98
+ const owner = entry.model.name;
99
+ add(mutations, roots.mutation, owner, 'plural', mutationFields(registry, entry));
100
+ add(fields, roots.query, owner, 'plural', rootFields(registry, entry));
101
+ }
102
+ // The models' types, which no function's or field's type may be named like.
103
+ const generated = generatedTypes(fields, mutations, typeOwners);
104
+ for (const [name, owner] of typeOwners) {
105
+ if (name in generated && generated[name] !== named(name)) {
106
+ throw taken(name, owner);
107
+ }
108
+ }
109
+ for (const { query, name, owner, field } of functions) {
110
+ const [into, owners] = query
111
+ ? [fields, roots.query]
112
+ : [mutations, roots.mutation];
113
+ add(into, owners, owner, 'name', { [name]: field });
114
+ }
115
+ if (Object.keys(fields).length === 0) {
116
+ throw new Refused('graphqlPlugin: no query to expose; each model has .with({ graphql: false }) and no function is a query.');
117
+ }
118
+ return new GraphQLSchema({
119
+ query: new GraphQLObjectType({ name: 'Query', fields }),
120
+ mutation: Object.keys(mutations).length === 0
121
+ ? undefined
122
+ : new GraphQLObjectType({ name: 'Mutation', fields: mutations }),
123
+ subscription: Object.keys(subscriptions).length === 0
124
+ ? undefined
125
+ : new GraphQLObjectType({
126
+ name: 'Subscription',
127
+ fields: subscriptions,
128
+ }),
129
+ });
130
+ }
@@ -0,0 +1,24 @@
1
+ import type { FieldNode, GraphQLObjectType, GraphQLResolveInfo, SelectionSetNode } from 'graphql';
2
+ import type { Args, Registry } from './registry.ts';
3
+ export type Info = Pick<GraphQLResolveInfo, 'fragments' | 'variableValues'>;
4
+ /** A field's name, or its alias: the key of its value in the response. */
5
+ export type Key = (node: FieldNode) => string;
6
+ /** Fields under one key, the first of them included. */
7
+ export type Fields = readonly [FieldNode, ...FieldNode[]];
8
+ /** A field or an operation: what selects fields. */
9
+ export type Selecting = {
10
+ readonly selectionSet?: SelectionSetNode | undefined;
11
+ };
12
+ /** The fields under some nodes, by name, through fragments and directives. */
13
+ export declare function grouped(info: Info, nodes: readonly Selecting[], key?: Key): ReadonlyMap<string, Fields>;
14
+ /** Under a connection, the nodes its edges select. */
15
+ export declare function nodesOf(info: Info, nodes: readonly FieldNode[]): FieldNode[];
16
+ /** What a root list selects of its rows: under `edges { node }`. */
17
+ export declare function rowNodes(registry: Registry, info: GraphQLResolveInfo): readonly FieldNode[];
18
+ /** The kernel's `select` for what a query asks of a model's rows. */
19
+ export declare function selectOf(registry: Registry, info: Info, type: GraphQLObjectType, nodes: readonly FieldNode[], master: boolean): Args;
20
+ /** `context.select(info)`, for a field that returns a model or a list of one. */
21
+ export declare function selectFor(registry: Registry, master: boolean): (info: GraphQLResolveInfo) => Args & {
22
+ readonly id: true;
23
+ };
24
+ //# sourceMappingURL=select.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"select.d.ts","sourceRoot":"","sources":["../src/select.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACX,SAAS,EACT,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAChB,MAAM,SAAS,CAAA;AAchB,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAEnD,MAAM,MAAM,IAAI,GAAG,IAAI,CAAC,kBAAkB,EAAE,WAAW,GAAG,gBAAgB,CAAC,CAAA;AAC3E,0EAA0E;AAC1E,MAAM,MAAM,GAAG,GAAG,CAAC,IAAI,EAAE,SAAS,KAAK,MAAM,CAAA;AAC7C,wDAAwD;AACxD,MAAM,MAAM,MAAM,GAAG,SAAS,CAAC,SAAS,EAAE,GAAG,SAAS,EAAE,CAAC,CAAA;AACzD,oDAAoD;AACpD,MAAM,MAAM,SAAS,GAAG;IAAE,QAAQ,CAAC,YAAY,CAAC,EAAE,gBAAgB,GAAG,SAAS,CAAA;CAAE,CAAA;AA6ChF,8EAA8E;AAC9E,wBAAgB,OAAO,CACtB,IAAI,EAAE,IAAI,EACV,KAAK,EAAE,SAAS,SAAS,EAAE,EAC3B,GAAG,GAAE,GAAY,GACf,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAK7B;AAwBD,sDAAsD;AACtD,wBAAgB,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,SAAS,EAAE,GAAG,SAAS,EAAE,CAG5E;AAED,oEAAoE;AACpE,wBAAgB,QAAQ,CACvB,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,kBAAkB,GACtB,SAAS,SAAS,EAAE,CAItB;AAqBD,qEAAqE;AACrE,wBAAgB,QAAQ,CACvB,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,IAAI,EACV,IAAI,EAAE,iBAAiB,EACvB,KAAK,EAAE,SAAS,SAAS,EAAE,EAC3B,MAAM,EAAE,OAAO,GACb,IAAI,CA2DN;AAED,iFAAiF;AACjF,wBAAgB,SAAS,CACxB,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,OAAO,GACb,CAAC,IAAI,EAAE,kBAAkB,KAAK,IAAI,GAAG;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAA;CAAE,CAa5D"}