@ultimat3/entity 0.0.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +126 -40
- package/package.json +4 -3
- package/src/column.d.ts +28 -0
- package/src/column.d.ts.map +1 -0
- package/src/column.js +75 -0
- package/src/column.js.map +1 -0
- package/src/column.ts +134 -0
- package/src/columns.d.ts +39 -0
- package/src/columns.d.ts.map +1 -0
- package/src/columns.js +136 -0
- package/src/columns.js.map +1 -0
- package/src/columns.ts +164 -217
- package/src/cursor.ts +187 -0
- package/src/database.d.ts +21 -0
- package/src/database.d.ts.map +1 -0
- package/src/database.js +38 -0
- package/src/database.js.map +1 -0
- package/src/database.ts +62 -0
- package/src/describe.d.ts +16 -0
- package/src/describe.d.ts.map +1 -0
- package/src/describe.js +79 -0
- package/src/describe.js.map +1 -0
- package/src/describe.ts +106 -0
- package/src/entity.d.ts +58 -0
- package/src/entity.d.ts.map +1 -0
- package/src/entity.js +160 -0
- package/src/entity.js.map +1 -0
- package/src/entity.ts +246 -99
- package/src/errors.d.ts +18 -0
- package/src/errors.d.ts.map +1 -0
- package/src/errors.js +59 -0
- package/src/errors.js.map +1 -0
- package/src/errors.ts +27 -6
- package/src/expr.d.ts +41 -0
- package/src/expr.d.ts.map +1 -0
- package/src/expr.js +94 -0
- package/src/expr.js.map +1 -0
- package/src/expr.ts +231 -0
- package/src/index.d.ts +23 -0
- package/src/index.d.ts.map +1 -0
- package/src/index.js +12 -0
- package/src/index.js.map +1 -0
- package/src/index.ts +36 -20
- package/src/invariants.d.ts +36 -0
- package/src/invariants.d.ts.map +1 -0
- package/src/invariants.js +53 -0
- package/src/invariants.js.map +1 -0
- package/src/invariants.ts +53 -49
- package/src/pg-driver.ts +154 -0
- package/src/pg-row.ts +110 -0
- package/src/pg-sql.ts +162 -0
- package/src/plan.ts +82 -0
- package/src/query.d.ts +30 -0
- package/src/query.d.ts.map +1 -0
- package/src/query.js +74 -0
- package/src/query.js.map +1 -0
- package/src/query.ts +144 -0
- package/src/registry.d.ts +45 -0
- package/src/registry.d.ts.map +1 -0
- package/src/registry.js +26 -0
- package/src/registry.js.map +1 -0
- package/src/registry.ts +8 -5
- package/src/repo.d.ts +54 -0
- package/src/repo.d.ts.map +1 -0
- package/src/repo.js +203 -0
- package/src/repo.js.map +1 -0
- package/src/repo.ts +0 -0
- package/src/seed.d.ts +20 -0
- package/src/seed.d.ts.map +1 -0
- package/src/seed.js +43 -0
- package/src/seed.js.map +1 -0
- package/src/seed.ts +69 -0
- package/src/tenancy.d.ts +41 -0
- package/src/tenancy.d.ts.map +1 -0
- package/src/tenancy.js +57 -0
- package/src/tenancy.js.map +1 -0
- package/src/tenancy.ts +68 -19
- package/src/types.d.ts +99 -0
- package/src/types.d.ts.map +1 -0
- package/src/types.js +8 -0
- package/src/types.js.map +1 -0
- package/src/types.ts +94 -44
- package/src/view.ts +97 -0
package/src/plan.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Single responsibility: turn repository arguments into the `QueryPlan` a driver executes.
|
|
2
|
+
// It sits outside both drivers because memory and Postgres must agree on what a call means —
|
|
3
|
+
// which rows are in scope, what the total sort order is, how big a page is. A guard only one
|
|
4
|
+
// driver applies is worse than none: the test passes and production leaks another tenant's rows.
|
|
5
|
+
|
|
6
|
+
import type { EntityCore } from './entity';
|
|
7
|
+
import { invariantViolated } from './errors';
|
|
8
|
+
import type { FindManyArgs, RepoOptions } from './repo';
|
|
9
|
+
import type { Predicate, QueryPlan } from './tenancy';
|
|
10
|
+
import { assertScoped } from './tenancy';
|
|
11
|
+
|
|
12
|
+
/** A page is bounded by default; an unbounded read is a production incident waiting for traffic. */
|
|
13
|
+
export const DEFAULT_PAGE_SIZE = 50;
|
|
14
|
+
|
|
15
|
+
/** Id-addressed operations need exactly one key. A composite key is a `findMany({ where })`. */
|
|
16
|
+
export const singleKeyOf = <Row>(entity: EntityCore<Row>, operation: string): string => {
|
|
17
|
+
const [only] = entity.$primaryKey;
|
|
18
|
+
if (entity.$primaryKey.length !== 1 || only === undefined) {
|
|
19
|
+
throw invariantViolated(
|
|
20
|
+
entity.$name,
|
|
21
|
+
operation,
|
|
22
|
+
`${entity.$name} has a composite primary key (${entity.$primaryKey.join(', ')}) — ` +
|
|
23
|
+
'use findMany({ where }) instead of an id',
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
return only;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const planFor = <Row>(entity: EntityCore<Row>, args: FindManyArgs): QueryPlan => {
|
|
30
|
+
const scoped =
|
|
31
|
+
args.orgId === undefined || entity.$tenantColumn === null
|
|
32
|
+
? []
|
|
33
|
+
: [{ column: entity.$tenantColumn, op: 'eq', value: args.orgId } satisfies Predicate];
|
|
34
|
+
const ordered = args.orderBy ?? [];
|
|
35
|
+
return {
|
|
36
|
+
entity: entity.$name,
|
|
37
|
+
where: [...(args.where ?? []), ...scoped],
|
|
38
|
+
// The primary key is always the final sort key: a cursor needs a total order, or two
|
|
39
|
+
// rows with the same sort value straddle a page boundary.
|
|
40
|
+
orderBy: [
|
|
41
|
+
...ordered,
|
|
42
|
+
...entity.$primaryKey
|
|
43
|
+
.filter((property) => !ordered.some((entry) => entry.column === property))
|
|
44
|
+
.map((property) => ({ column: property, direction: 'asc' as const })),
|
|
45
|
+
],
|
|
46
|
+
limit: args.limit ?? DEFAULT_PAGE_SIZE,
|
|
47
|
+
...(args.cursor === undefined || args.cursor === null ? {} : { cursor: args.cursor }),
|
|
48
|
+
...(args.select === undefined ? {} : { select: args.select }),
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** The plan for a read. Throws `X_TENANCY_UNSCOPED` before a single row is considered. */
|
|
53
|
+
export const readPlan = <Row>(
|
|
54
|
+
entity: EntityCore<Row>,
|
|
55
|
+
args: FindManyArgs,
|
|
56
|
+
operation: string,
|
|
57
|
+
): QueryPlan => {
|
|
58
|
+
const plan = planFor(entity, args);
|
|
59
|
+
assertScoped(entity.$name, entity.$tenantColumn, operation, plan);
|
|
60
|
+
return plan;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The plan for an id-addressed write. A write is a query too: without the same guard,
|
|
65
|
+
* `update(id, patch)` on a tenant-scoped entity is a cross-tenant write that no read path
|
|
66
|
+
* would ever have allowed.
|
|
67
|
+
*/
|
|
68
|
+
export const idPlan = <Row>(
|
|
69
|
+
entity: EntityCore<Row>,
|
|
70
|
+
id: string,
|
|
71
|
+
options: RepoOptions | undefined,
|
|
72
|
+
operation: string,
|
|
73
|
+
): QueryPlan =>
|
|
74
|
+
readPlan(
|
|
75
|
+
entity,
|
|
76
|
+
{
|
|
77
|
+
...options,
|
|
78
|
+
where: [{ column: singleKeyOf(entity, operation), op: 'eq', value: id }],
|
|
79
|
+
limit: 1,
|
|
80
|
+
},
|
|
81
|
+
operation,
|
|
82
|
+
);
|
package/src/query.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { EntityCore } from './entity';
|
|
2
|
+
import type { Page, Repo, RepoOptions } from './repo';
|
|
3
|
+
import type { Operator, QueryPlan, SortDirection } from './tenancy';
|
|
4
|
+
import type { ColumnMap, Insertable } from './types';
|
|
5
|
+
export interface ReadBuilder<Row> {
|
|
6
|
+
/** Equality on the columns given. `where({ orgId })` is what satisfies the tenancy guard. */
|
|
7
|
+
where(filter: Partial<Row>): ReadBuilder<Row>;
|
|
8
|
+
andWhere(column: keyof Row & string, op: Operator, value: unknown): ReadBuilder<Row>;
|
|
9
|
+
orderBy(column: keyof Row & string, direction?: SortDirection): ReadBuilder<Row>;
|
|
10
|
+
limit(rows: number): ReadBuilder<Row>;
|
|
11
|
+
/** The cursor from the previous page. */
|
|
12
|
+
after(cursor: string | null): ReadBuilder<Row>;
|
|
13
|
+
select<K extends keyof Row & string>(fields: {
|
|
14
|
+
readonly [P in K]: true;
|
|
15
|
+
}): ReadBuilder<Pick<Row, K>>;
|
|
16
|
+
/** The terminal: one bounded page and the cursor that continues it. */
|
|
17
|
+
page(): Promise<Page<Row>>;
|
|
18
|
+
all(): Promise<readonly Row[]>;
|
|
19
|
+
one(): Promise<Row | null>;
|
|
20
|
+
count(): Promise<number>;
|
|
21
|
+
/** The plan this chain describes. Safe to log — `describePlan()` elides values. */
|
|
22
|
+
plan(): QueryPlan;
|
|
23
|
+
}
|
|
24
|
+
export interface Table<Row, C extends ColumnMap = ColumnMap> extends ReadBuilder<Row> {
|
|
25
|
+
insert(values: Insertable<C>, options?: RepoOptions): Promise<Row>;
|
|
26
|
+
update(id: string, patch: Partial<Row>, options?: RepoOptions): Promise<Row>;
|
|
27
|
+
delete(id: string, options?: RepoOptions): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
export declare const tableFor: <Row, C extends ColumnMap>(entity: EntityCore<Row, C>, repo: Repo<Row>) => Table<Row, C>;
|
|
30
|
+
//# sourceMappingURL=query.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query.d.ts","sourceRoot":"","sources":["query.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AACtD,OAAO,KAAK,EAAE,QAAQ,EAAa,SAAS,EAAE,aAAa,EAAW,MAAM,WAAW,CAAC;AACxF,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAErD,MAAM,WAAW,WAAW,CAAC,GAAG;IAC9B,6FAA6F;IAC7F,KAAK,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;IAC9C,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,GAAG,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;IACrF,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,GAAG,MAAM,EAAE,SAAS,CAAC,EAAE,aAAa,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;IACjF,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;IACtC,yCAAyC;IACzC,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;IAC/C,MAAM,CAAC,CAAC,SAAS,MAAM,GAAG,GAAG,MAAM,EACjC,MAAM,EAAE;QAAE,QAAQ,EAAE,CAAC,IAAI,CAAC,GAAG,IAAI;KAAE,GAClC,WAAW,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC;IAC7B,uEAAuE;IACvE,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IAC3B,GAAG,IAAI,OAAO,CAAC,SAAS,GAAG,EAAE,CAAC,CAAC;IAC/B,GAAG,IAAI,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC;IAC3B,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IACzB,mFAAmF;IACnF,IAAI,IAAI,SAAS,CAAC;CACnB;AAED,MAAM,WAAW,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,SAAS,GAAG,SAAS,CAAE,SAAQ,WAAW,CAAC,GAAG,CAAC;IACnF,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;IACnE,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;IAC7E,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1D;AAkGD,eAAO,MAAM,QAAQ,GAAI,GAAG,EAAE,CAAC,SAAS,SAAS,UACvC,UAAU,CAAC,GAAG,EAAE,CAAC,CAAC,QACpB,IAAI,CAAC,GAAG,CAAC,KACd,KAAK,CAAC,GAAG,EAAE,CAAC,CAKb,CAAC"}
|
package/src/query.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// The chainable read. Every chain terminates in a cursor page — `page()` returns rows plus the
|
|
2
|
+
// cursor for the next one, and `all()`/`one()` are that page's rows. There is no `offset()` and
|
|
3
|
+
// there will not be one: under concurrent writes an insert before the offset shifts every later
|
|
4
|
+
// page, so a client silently skips and repeats rows.
|
|
5
|
+
const EMPTY = { where: [], orderBy: [], limit: 50, cursor: null, select: undefined };
|
|
6
|
+
const asRecord = (value) => typeof value === 'object' && value !== null ? value : {};
|
|
7
|
+
const builder = (entity, repo, state, pick) => {
|
|
8
|
+
const next = (patch) => builder(entity, repo, { ...state, ...patch }, pick);
|
|
9
|
+
const args = () => ({
|
|
10
|
+
where: state.where,
|
|
11
|
+
orderBy: state.orderBy,
|
|
12
|
+
limit: state.limit,
|
|
13
|
+
cursor: state.cursor,
|
|
14
|
+
...(state.select === undefined ? {} : { select: state.select }),
|
|
15
|
+
});
|
|
16
|
+
return {
|
|
17
|
+
where: (filter) => next({
|
|
18
|
+
where: [
|
|
19
|
+
...state.where,
|
|
20
|
+
...Object.entries(asRecord(filter)).map(([column, value]) => ({ column, op: 'eq', value })),
|
|
21
|
+
],
|
|
22
|
+
}),
|
|
23
|
+
andWhere: (column, op, value) => next({ where: [...state.where, { column, op, value }] }),
|
|
24
|
+
orderBy: (column, direction = 'asc') => next({ orderBy: [...state.orderBy, { column, direction }] }),
|
|
25
|
+
limit: (rows) => next({ limit: rows }),
|
|
26
|
+
after: (cursor) => next({ cursor }),
|
|
27
|
+
select(fields) {
|
|
28
|
+
// The predicate is what carries the literal key type through `Object.keys`.
|
|
29
|
+
const keys = Object.keys(fields).filter((key) => Object.hasOwn(fields, key));
|
|
30
|
+
return builder(entity, repo, { ...state, select: keys }, (row) => {
|
|
31
|
+
const source = pick(row);
|
|
32
|
+
const picked = {};
|
|
33
|
+
for (const key of keys)
|
|
34
|
+
picked[key] = source[key];
|
|
35
|
+
return picked;
|
|
36
|
+
});
|
|
37
|
+
},
|
|
38
|
+
page: async () => {
|
|
39
|
+
const result = await repo.findMany(args());
|
|
40
|
+
return { rows: result.rows.map(pick), nextCursor: result.nextCursor };
|
|
41
|
+
},
|
|
42
|
+
all: async () => (await repo.findMany(args())).rows.map(pick),
|
|
43
|
+
one: async () => {
|
|
44
|
+
const { rows } = await repo.findMany({ ...args(), limit: 1 });
|
|
45
|
+
const row = rows[0];
|
|
46
|
+
return row === undefined ? null : pick(row);
|
|
47
|
+
},
|
|
48
|
+
count: () => repo.count(args()),
|
|
49
|
+
plan: () => ({
|
|
50
|
+
entity: entity.$name,
|
|
51
|
+
where: state.where,
|
|
52
|
+
orderBy: state.orderBy,
|
|
53
|
+
limit: state.limit,
|
|
54
|
+
...(state.cursor === null ? {} : { cursor: state.cursor }),
|
|
55
|
+
...(state.select === undefined ? {} : { select: state.select }),
|
|
56
|
+
}),
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
/** Columns declared `onUpdateNow()` are written by the framework, never by the caller. */
|
|
60
|
+
const touch = (entity, patch) => {
|
|
61
|
+
const stamped = {};
|
|
62
|
+
for (const [property, column] of Object.entries(entity.$columns)) {
|
|
63
|
+
if (column.$meta.onUpdate !== undefined)
|
|
64
|
+
stamped[property] = new Date();
|
|
65
|
+
}
|
|
66
|
+
return Object.assign({}, patch, stamped);
|
|
67
|
+
};
|
|
68
|
+
export const tableFor = (entity, repo) => ({
|
|
69
|
+
...builder(entity, repo, EMPTY, (row) => row),
|
|
70
|
+
insert: (values, options) => repo.insert(entity.$parse(values), options),
|
|
71
|
+
update: (id, patch, options) => repo.update(id, touch(entity, patch), options),
|
|
72
|
+
delete: (id, options) => repo.delete(id, options),
|
|
73
|
+
});
|
|
74
|
+
//# sourceMappingURL=query.js.map
|
package/src/query.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query.js","sourceRoot":"","sources":["query.ts"],"names":[],"mappings":"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"}
|
package/src/query.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// The chainable read. Every chain terminates in a cursor page — `page()` returns rows plus the
|
|
2
|
+
// cursor for the next one, and `all()`/`one()` are that page's rows. There is no `offset()` and
|
|
3
|
+
// there will not be one: under concurrent writes an insert before the offset shifts every later
|
|
4
|
+
// page, so a client silently skips and repeats rows.
|
|
5
|
+
|
|
6
|
+
import type { EntityCore } from './entity';
|
|
7
|
+
import type { Page, Repo, RepoOptions } from './repo';
|
|
8
|
+
import type { Operator, Predicate, QueryPlan, SortDirection, SortKey } from './tenancy';
|
|
9
|
+
import type { ColumnMap, Insertable } from './types';
|
|
10
|
+
|
|
11
|
+
export interface ReadBuilder<Row> {
|
|
12
|
+
/** Equality on the columns given. `where({ orgId })` is what satisfies the tenancy guard. */
|
|
13
|
+
where(filter: Partial<Row>): ReadBuilder<Row>;
|
|
14
|
+
andWhere(column: keyof Row & string, op: Operator, value: unknown): ReadBuilder<Row>;
|
|
15
|
+
orderBy(column: keyof Row & string, direction?: SortDirection): ReadBuilder<Row>;
|
|
16
|
+
limit(rows: number): ReadBuilder<Row>;
|
|
17
|
+
/** The cursor from the previous page. */
|
|
18
|
+
after(cursor: string | null): ReadBuilder<Row>;
|
|
19
|
+
select<K extends keyof Row & string>(
|
|
20
|
+
fields: { readonly [P in K]: true },
|
|
21
|
+
): ReadBuilder<Pick<Row, K>>;
|
|
22
|
+
/** The terminal: one bounded page and the cursor that continues it. */
|
|
23
|
+
page(): Promise<Page<Row>>;
|
|
24
|
+
all(): Promise<readonly Row[]>;
|
|
25
|
+
one(): Promise<Row | null>;
|
|
26
|
+
count(): Promise<number>;
|
|
27
|
+
/** The plan this chain describes. Safe to log — `describePlan()` elides values. */
|
|
28
|
+
plan(): QueryPlan;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface Table<Row, C extends ColumnMap = ColumnMap> extends ReadBuilder<Row> {
|
|
32
|
+
insert(values: Insertable<C>, options?: RepoOptions): Promise<Row>;
|
|
33
|
+
update(id: string, patch: Partial<Row>, options?: RepoOptions): Promise<Row>;
|
|
34
|
+
delete(id: string, options?: RepoOptions): Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface State {
|
|
38
|
+
readonly where: readonly Predicate[];
|
|
39
|
+
readonly orderBy: readonly SortKey[];
|
|
40
|
+
readonly limit: number;
|
|
41
|
+
readonly cursor: string | null;
|
|
42
|
+
readonly select: readonly string[] | undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const EMPTY: State = { where: [], orderBy: [], limit: 50, cursor: null, select: undefined };
|
|
46
|
+
|
|
47
|
+
const asRecord = (value: unknown): Readonly<Record<string, unknown>> =>
|
|
48
|
+
typeof value === 'object' && value !== null ? (value as Record<string, unknown>) : {};
|
|
49
|
+
|
|
50
|
+
const builder = <Source, Row>(
|
|
51
|
+
entity: EntityCore<Source>,
|
|
52
|
+
repo: Repo<Source>,
|
|
53
|
+
state: State,
|
|
54
|
+
pick: (row: Source) => Row,
|
|
55
|
+
): ReadBuilder<Row> => {
|
|
56
|
+
const next = (patch: Partial<State>): ReadBuilder<Row> =>
|
|
57
|
+
builder(entity, repo, { ...state, ...patch }, pick);
|
|
58
|
+
|
|
59
|
+
const args = () => ({
|
|
60
|
+
where: state.where,
|
|
61
|
+
orderBy: state.orderBy,
|
|
62
|
+
limit: state.limit,
|
|
63
|
+
cursor: state.cursor,
|
|
64
|
+
...(state.select === undefined ? {} : { select: state.select }),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
where: (filter) =>
|
|
69
|
+
next({
|
|
70
|
+
where: [
|
|
71
|
+
...state.where,
|
|
72
|
+
...Object.entries(asRecord(filter)).map(
|
|
73
|
+
([column, value]): Predicate => ({ column, op: 'eq', value }),
|
|
74
|
+
),
|
|
75
|
+
],
|
|
76
|
+
}),
|
|
77
|
+
|
|
78
|
+
andWhere: (column, op, value) => next({ where: [...state.where, { column, op, value }] }),
|
|
79
|
+
|
|
80
|
+
orderBy: (column, direction = 'asc') =>
|
|
81
|
+
next({ orderBy: [...state.orderBy, { column, direction }] }),
|
|
82
|
+
|
|
83
|
+
limit: (rows) => next({ limit: rows }),
|
|
84
|
+
|
|
85
|
+
after: (cursor) => next({ cursor }),
|
|
86
|
+
|
|
87
|
+
select<K extends keyof Row & string>(fields: { readonly [P in K]: true }) {
|
|
88
|
+
// The predicate is what carries the literal key type through `Object.keys`.
|
|
89
|
+
const keys = Object.keys(fields).filter((key): key is K => Object.hasOwn(fields, key));
|
|
90
|
+
return builder<Source, Pick<Row, K>>(entity, repo, { ...state, select: keys }, (row) => {
|
|
91
|
+
const source = pick(row);
|
|
92
|
+
const picked = {} as Pick<Row, K>;
|
|
93
|
+
for (const key of keys) picked[key] = source[key];
|
|
94
|
+
return picked;
|
|
95
|
+
});
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
page: async () => {
|
|
99
|
+
const result = await repo.findMany(args());
|
|
100
|
+
return { rows: result.rows.map(pick), nextCursor: result.nextCursor };
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
all: async () => (await repo.findMany(args())).rows.map(pick),
|
|
104
|
+
|
|
105
|
+
one: async () => {
|
|
106
|
+
const { rows } = await repo.findMany({ ...args(), limit: 1 });
|
|
107
|
+
const row = rows[0];
|
|
108
|
+
return row === undefined ? null : pick(row);
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
count: () => repo.count(args()),
|
|
112
|
+
|
|
113
|
+
plan: (): QueryPlan => ({
|
|
114
|
+
entity: entity.$name,
|
|
115
|
+
where: state.where,
|
|
116
|
+
orderBy: state.orderBy,
|
|
117
|
+
limit: state.limit,
|
|
118
|
+
...(state.cursor === null ? {} : { cursor: state.cursor }),
|
|
119
|
+
...(state.select === undefined ? {} : { select: state.select }),
|
|
120
|
+
}),
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/** Columns declared `onUpdateNow()` are written by the framework, never by the caller. */
|
|
125
|
+
const touch = <Row>(entity: EntityCore<Row>, patch: Partial<Row>): Partial<Row> => {
|
|
126
|
+
const stamped: Record<string, unknown> = {};
|
|
127
|
+
for (const [property, column] of Object.entries(entity.$columns)) {
|
|
128
|
+
if (column.$meta.onUpdate !== undefined) stamped[property] = new Date();
|
|
129
|
+
}
|
|
130
|
+
return Object.assign({}, patch, stamped);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// Every write is `async`, matching the repository contract: a failing call rejects and never
|
|
134
|
+
// throws synchronously. `$parse` throws — without the wrapper, a bad row escapes at call time
|
|
135
|
+
// while a bad id rejects, and every call site would need two error paths for one mistake.
|
|
136
|
+
export const tableFor = <Row, C extends ColumnMap>(
|
|
137
|
+
entity: EntityCore<Row, C>,
|
|
138
|
+
repo: Repo<Row>,
|
|
139
|
+
): Table<Row, C> => ({
|
|
140
|
+
...builder<Row, Row>(entity, repo, EMPTY, (row) => row),
|
|
141
|
+
insert: async (values, options) => repo.insert(entity.$parse(values), options),
|
|
142
|
+
update: async (id, patch, options) => repo.update(id, touch(entity, patch), options),
|
|
143
|
+
delete: async (id, options) => repo.delete(id, options),
|
|
144
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { InvariantKind } from './invariants';
|
|
2
|
+
export interface ColumnDescription {
|
|
3
|
+
readonly property: string;
|
|
4
|
+
readonly column: string;
|
|
5
|
+
readonly kind: string;
|
|
6
|
+
readonly notNull: boolean;
|
|
7
|
+
readonly primaryKey: boolean;
|
|
8
|
+
readonly unique: boolean;
|
|
9
|
+
readonly hasDefault: boolean;
|
|
10
|
+
readonly check: string | null;
|
|
11
|
+
readonly references: string | null;
|
|
12
|
+
}
|
|
13
|
+
export interface InvariantDescription {
|
|
14
|
+
readonly name: string;
|
|
15
|
+
readonly kind: InvariantKind;
|
|
16
|
+
readonly message: string;
|
|
17
|
+
/** `null` for an `assert`: a JS predicate the database was never told about. */
|
|
18
|
+
readonly sql: string | null;
|
|
19
|
+
readonly where: string | null;
|
|
20
|
+
}
|
|
21
|
+
export interface EntityDescription {
|
|
22
|
+
readonly name: string;
|
|
23
|
+
readonly table: string;
|
|
24
|
+
readonly primaryKey: readonly string[];
|
|
25
|
+
readonly columns: readonly ColumnDescription[];
|
|
26
|
+
readonly invariants: readonly InvariantDescription[];
|
|
27
|
+
readonly indexes: readonly string[];
|
|
28
|
+
readonly tags: readonly string[];
|
|
29
|
+
readonly cacheTag: string;
|
|
30
|
+
readonly softDelete: boolean;
|
|
31
|
+
readonly orgScoped: boolean;
|
|
32
|
+
}
|
|
33
|
+
export interface RegistryEntry {
|
|
34
|
+
readonly name: string;
|
|
35
|
+
readonly tableName: string;
|
|
36
|
+
describe(): EntityDescription;
|
|
37
|
+
}
|
|
38
|
+
export declare const registerEntity: <E extends RegistryEntry>(entry: E) => E;
|
|
39
|
+
export declare const getEntity: (name: string) => RegistryEntry | undefined;
|
|
40
|
+
export declare const entityNames: () => readonly string[];
|
|
41
|
+
/** Deterministic order: the manifest is a build artefact and must diff cleanly. */
|
|
42
|
+
export declare const describeEntities: () => readonly EntityDescription[];
|
|
43
|
+
/** Test seam. Production code never unregisters an entity. */
|
|
44
|
+
export declare const clearRegistry: () => void;
|
|
45
|
+
//# sourceMappingURL=registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["registry.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAElD,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CACpC;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,gFAAgF;IAChF,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IACvC,QAAQ,CAAC,OAAO,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAC/C,QAAQ,CAAC,UAAU,EAAE,SAAS,oBAAoB,EAAE,CAAC;IACrD,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,IAAI,iBAAiB,CAAC;CAC/B;AAID,eAAO,MAAM,cAAc,GAAI,CAAC,SAAS,aAAa,SAAS,CAAC,KAAG,CAOlE,CAAC;AAEF,eAAO,MAAM,SAAS,SAAU,MAAM,KAAG,aAAa,GAAG,SAA+B,CAAC;AAEzF,eAAO,MAAM,WAAW,QAAO,SAAS,MAAM,EAAiC,CAAC;AAEhF,mFAAmF;AACnF,eAAO,MAAM,gBAAgB,QAAO,SAAS,iBAAiB,EAK1D,CAAC;AAEL,8DAA8D;AAC9D,eAAO,MAAM,aAAa,QAAO,IAAwB,CAAC"}
|
package/src/registry.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// The entity registry. Every `entity()` call registers here, which is what makes
|
|
2
|
+
// `x.manifest.json`, the admin dashboard generator and the migration emitter able to see the
|
|
3
|
+
// whole domain without importing it — and what makes a duplicate name a build error rather
|
|
4
|
+
// than a silent last-one-wins.
|
|
5
|
+
import { entityDuplicate } from './errors';
|
|
6
|
+
const entities = new Map();
|
|
7
|
+
export const registerEntity = (entry) => {
|
|
8
|
+
const existing = entities.get(entry.name);
|
|
9
|
+
if (existing !== undefined && existing !== entry) {
|
|
10
|
+
throw entityDuplicate(entry.name, existing.tableName);
|
|
11
|
+
}
|
|
12
|
+
entities.set(entry.name, entry);
|
|
13
|
+
return entry;
|
|
14
|
+
};
|
|
15
|
+
export const getEntity = (name) => entities.get(name);
|
|
16
|
+
export const entityNames = () => [...entities.keys()].sort();
|
|
17
|
+
/** Deterministic order: the manifest is a build artefact and must diff cleanly. */
|
|
18
|
+
export const describeEntities = () => entityNames().map((name) => {
|
|
19
|
+
const entry = entities.get(name);
|
|
20
|
+
if (entry === undefined)
|
|
21
|
+
throw entityDuplicate(name, 'unknown');
|
|
22
|
+
return entry.describe();
|
|
23
|
+
});
|
|
24
|
+
/** Test seam. Production code never unregisters an entity. */
|
|
25
|
+
export const clearRegistry = () => entities.clear();
|
|
26
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["registry.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,6FAA6F;AAC7F,2FAA2F;AAC3F,+BAA+B;AAE/B,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AA2C3C,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAyB,CAAC;AAElD,MAAM,CAAC,MAAM,cAAc,GAAG,CAA0B,KAAQ,EAAK,EAAE;IACrE,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC1C,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,KAAK,KAAK,EAAE,CAAC;QACjD,MAAM,eAAe,CAAC,KAAK,CAAC,IAAI,EAAE,QAAQ,CAAC,SAAS,CAAC,CAAC;IACxD,CAAC;IACD,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAChC,OAAO,KAAK,CAAC;AACf,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,IAAY,EAA6B,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AAEzF,MAAM,CAAC,MAAM,WAAW,GAAG,GAAsB,EAAE,CAAC,CAAC,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;AAEhF,mFAAmF;AACnF,MAAM,CAAC,MAAM,gBAAgB,GAAG,GAAiC,EAAE,CACjE,WAAW,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;IACzB,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACjC,IAAI,KAAK,KAAK,SAAS;QAAE,MAAM,eAAe,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IAChE,OAAO,KAAK,CAAC,QAAQ,EAAE,CAAC;AAC1B,CAAC,CAAC,CAAC;AAEL,8DAA8D;AAC9D,MAAM,CAAC,MAAM,aAAa,GAAG,GAAS,EAAE,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC"}
|
package/src/registry.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// The entity registry. Every `entity()` call registers here, which is what makes
|
|
2
|
-
// `x.manifest.json`, the admin dashboard generator and the migration emitter able to
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// `x.manifest.json`, the admin dashboard generator and the migration emitter able to see the
|
|
3
|
+
// whole domain without importing it — and what makes a duplicate name a build error rather
|
|
4
|
+
// than a silent last-one-wins.
|
|
5
|
+
|
|
5
6
|
import { entityDuplicate } from './errors';
|
|
7
|
+
import type { InvariantKind } from './invariants';
|
|
6
8
|
|
|
7
9
|
export interface ColumnDescription {
|
|
8
10
|
readonly property: string;
|
|
@@ -18,9 +20,10 @@ export interface ColumnDescription {
|
|
|
18
20
|
|
|
19
21
|
export interface InvariantDescription {
|
|
20
22
|
readonly name: string;
|
|
21
|
-
readonly kind:
|
|
23
|
+
readonly kind: InvariantKind;
|
|
22
24
|
readonly message: string;
|
|
23
|
-
|
|
25
|
+
/** `null` for an `assert`: a JS predicate the database was never told about. */
|
|
26
|
+
readonly sql: string | null;
|
|
24
27
|
readonly where: string | null;
|
|
25
28
|
}
|
|
26
29
|
|
package/src/repo.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { EntityCore } from './entity';
|
|
2
|
+
import type { Predicate, SortKey } from './tenancy';
|
|
3
|
+
export interface Tx {
|
|
4
|
+
readonly id: string;
|
|
5
|
+
/** Registered by drivers so a failed transaction can undo in-memory effects. */
|
|
6
|
+
onRollback(undo: () => void): void;
|
|
7
|
+
}
|
|
8
|
+
export interface RepoOptions {
|
|
9
|
+
readonly tx?: Tx;
|
|
10
|
+
/** Required for tenant-scoped entities; the guard throws without it. */
|
|
11
|
+
readonly orgId?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface FindManyArgs extends RepoOptions {
|
|
14
|
+
readonly where?: readonly Predicate[];
|
|
15
|
+
readonly orderBy?: readonly SortKey[];
|
|
16
|
+
readonly limit?: number;
|
|
17
|
+
readonly cursor?: string | null;
|
|
18
|
+
readonly includeDeleted?: boolean;
|
|
19
|
+
readonly select?: readonly string[];
|
|
20
|
+
}
|
|
21
|
+
export interface Page<T> {
|
|
22
|
+
readonly rows: readonly T[];
|
|
23
|
+
/** Pass back as `cursor`. `null` means this was the last page. */
|
|
24
|
+
readonly nextCursor: string | null;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* `T` defaults to `unknown` so a row-agnostic consumer (the generated admin, the manifest
|
|
28
|
+
* emitter) can name the shape without knowing the entity.
|
|
29
|
+
*/
|
|
30
|
+
export interface Repo<T = unknown> {
|
|
31
|
+
findById(id: string, options?: RepoOptions): Promise<T | null>;
|
|
32
|
+
findMany(args?: FindManyArgs): Promise<Page<T>>;
|
|
33
|
+
insert(values: T, options?: RepoOptions): Promise<T>;
|
|
34
|
+
update(id: string, patch: Partial<T>, options?: RepoOptions): Promise<T>;
|
|
35
|
+
delete(id: string, options?: RepoOptions): Promise<void>;
|
|
36
|
+
count(args?: FindManyArgs): Promise<number>;
|
|
37
|
+
}
|
|
38
|
+
export interface Transactor {
|
|
39
|
+
run<R>(work: (tx: Tx) => Promise<R>): Promise<R>;
|
|
40
|
+
}
|
|
41
|
+
export declare const encodeCursor: (key: string, id: string) => string;
|
|
42
|
+
export declare const decodeCursor: (cursor: string) => {
|
|
43
|
+
key: string;
|
|
44
|
+
id: string;
|
|
45
|
+
} | null;
|
|
46
|
+
/**
|
|
47
|
+
* The default driver: correct semantics, no database. `x dev` uses it before the first
|
|
48
|
+
* migration and tests use it everywhere. Postgres is the production driver and implements
|
|
49
|
+
* this same interface.
|
|
50
|
+
*/
|
|
51
|
+
export declare const memoryRepo: <Row>(entity: EntityCore<Row>, seed?: readonly Row[]) => Repo<Row>;
|
|
52
|
+
/** In-memory transactor: undo closures registered by drivers run on failure. */
|
|
53
|
+
export declare const memoryTransactor: () => Transactor;
|
|
54
|
+
//# sourceMappingURL=repo.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repo.d.ts","sourceRoot":"","sources":["repo.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAE3C,OAAO,KAAK,EAAE,SAAS,EAAa,OAAO,EAAE,MAAM,WAAW,CAAC;AAG/D,MAAM,WAAW,EAAE;IACjB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,gFAAgF;IAChF,UAAU,CAAC,IAAI,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;CACpC;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;IACjB,wEAAwE;IACxE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,YAAa,SAAQ,WAAW;IAC/C,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,SAAS,EAAE,CAAC;IACtC,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,OAAO,EAAE,CAAC;IACtC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC;IAClC,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAED,MAAM,WAAW,IAAI,CAAC,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC,EAAE,CAAC;IAC5B,kEAAkE;IAClE,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CACpC;AAED;;;GAGG;AACH,MAAM,WAAW,IAAI,CAAC,CAAC,GAAG,OAAO;IAC/B,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAC/D,QAAQ,CAAC,IAAI,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IAChD,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACrD,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACzE,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzD,KAAK,CAAC,IAAI,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC7C;AAED,MAAM,WAAW,UAAU;IACzB,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CAClD;AAED,eAAO,MAAM,YAAY,QAAS,MAAM,MAAM,MAAM,KAAG,MACjB,CAAC;AAEvC,eAAO,MAAM,YAAY,WAAY,MAAM,KAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAAG,IAS3E,CAAC;AAiDF;;;;GAIG;AACH,eAAO,MAAM,UAAU,GAAI,GAAG,UAAU,UAAU,CAAC,GAAG,CAAC,SAAQ,SAAS,GAAG,EAAE,KAAQ,IAAI,CAAC,GAAG,CAoI5F,CAAC;AAIF,gFAAgF;AAChF,eAAO,MAAM,gBAAgB,QAAO,UAYlC,CAAC"}
|