@rebasepro/server-postgres 0.10.1-canary.d8d45b2 → 0.10.1-canary.ed8caed
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/dist/{ensure-collection-tables-CNlIONzj.js → ensure-collection-tables-BVvtkkRm.js} +10 -10
- package/dist/ensure-collection-tables-BVvtkkRm.js.map +1 -0
- package/dist/index.es.js +344 -119
- package/dist/index.es.js.map +1 -1
- package/dist/services/FetchService.d.ts +21 -8
- package/dist/services/PersistService.d.ts +12 -0
- package/dist/services/RelationService.d.ts +30 -0
- package/dist/services/nested-path.d.ts +59 -0
- package/dist/{src-B0v4IKaI.js → src-CBgtrPhJ.js} +8 -1
- package/dist/{src-B0v4IKaI.js.map → src-CBgtrPhJ.js.map} +1 -1
- package/dist/{src-DmsRg8MR.js → src-lcfUP4xg.js} +126 -76
- package/dist/src-lcfUP4xg.js.map +1 -0
- package/dist/utils/drizzle-conditions.d.ts +41 -0
- package/package.json +8 -9
- package/src/schema/doctor.ts +6 -6
- package/src/schema/generate-drizzle-schema-logic.ts +13 -9
- package/src/schema/generate-postgres-ddl-logic.ts +19 -12
- package/src/schema/introspect-db-inference.ts +13 -13
- package/src/schema/introspect-db-logic.ts +6 -6
- package/src/services/FetchService.ts +104 -114
- package/src/services/PersistService.ts +127 -85
- package/src/services/RelationService.ts +95 -6
- package/src/services/nested-path.ts +130 -0
- package/src/utils/drizzle-conditions.ts +143 -0
- package/dist/ensure-collection-tables-CNlIONzj.js.map +0 -1
- package/dist/src-DmsRg8MR.js.map +0 -1
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import { parseDataFromServer } from "../data-transformer";
|
|
16
16
|
import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
|
|
17
17
|
import { logger } from "@rebasepro/server";
|
|
18
|
+
import type { NestedPathHop } from "./nested-path";
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Typed wrapper for Drizzle dynamic query innerJoin.
|
|
@@ -333,6 +334,24 @@ export class RelationService {
|
|
|
333
334
|
throw new Error(`Relation '${relationKey}' not found in collection '${parentCollectionPath}'. Available relations: [${available}]`);
|
|
334
335
|
}
|
|
335
336
|
|
|
337
|
+
return this.countRelatedRows(parentCollection, parentId, relation, []);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Count the target rows a parent reaches through `relation`, narrowed by
|
|
342
|
+
* `additionalFilters` (conditions on the target table).
|
|
343
|
+
*
|
|
344
|
+
* Shared by the public count and by {@link isRelated}, so "how many children
|
|
345
|
+
* does this parent have" and "is this row one of them" are answered by the
|
|
346
|
+
* same join — a membership test that reconstructed the join separately would
|
|
347
|
+
* be free to disagree with the listing it is supposed to gate.
|
|
348
|
+
*/
|
|
349
|
+
private async countRelatedRows(
|
|
350
|
+
parentCollection: CollectionConfig,
|
|
351
|
+
parentId: string | number,
|
|
352
|
+
relation: Relation,
|
|
353
|
+
additionalFilters: SQL[]
|
|
354
|
+
): Promise<number> {
|
|
336
355
|
const targetCollection = relation.target();
|
|
337
356
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
338
357
|
const targetPks = requirePrimaryKeys(targetCollection, this.registry);
|
|
@@ -350,9 +369,6 @@ export class RelationService {
|
|
|
350
369
|
// Start count with distinct to avoid duplicates from junction tables
|
|
351
370
|
let query = this.db.select({ count: sql<number>`count(distinct ${targetIdField})` }).from(targetTable).$dynamic();
|
|
352
371
|
|
|
353
|
-
// Build additional filter conditions
|
|
354
|
-
const additionalFilters: SQL[] = [];
|
|
355
|
-
|
|
356
372
|
// Use unified count query builder from DrizzleConditionBuilder
|
|
357
373
|
query = DrizzleConditionBuilder.buildRelationCountQuery(
|
|
358
374
|
query,
|
|
@@ -370,6 +386,75 @@ export class RelationService {
|
|
|
370
386
|
return Number(result[0]?.count || 0);
|
|
371
387
|
}
|
|
372
388
|
|
|
389
|
+
/**
|
|
390
|
+
* Whether `targetId` is actually reachable from the parent named in `hop`.
|
|
391
|
+
*
|
|
392
|
+
* A nested address like `authors/1/posts/43` used to resolve to the target
|
|
393
|
+
* collection and then match on the primary key alone, so the parent segment
|
|
394
|
+
* decided nothing: the row came back, and was updated or deleted, whoever it
|
|
395
|
+
* belonged to. Reads, updates and deletes now all gate on this.
|
|
396
|
+
*/
|
|
397
|
+
async isRelated(hop: NestedPathHop, targetId: string | number): Promise<boolean> {
|
|
398
|
+
const targetTable = getTableForCollection(hop.targetCollection, this.registry);
|
|
399
|
+
const targetPks = requirePrimaryKeys(hop.targetCollection, this.registry);
|
|
400
|
+
const parsedTargetId = parseIdValues(targetId, targetPks);
|
|
401
|
+
|
|
402
|
+
const identity: SQL[] = targetPks.map(pk => {
|
|
403
|
+
const column = targetTable[pk.fieldName as keyof typeof targetTable] as AnyPgColumn;
|
|
404
|
+
if (!column) {
|
|
405
|
+
throw new Error(`ID field '${pk.fieldName}' not found in table for collection '${hop.targetCollection.slug}'`);
|
|
406
|
+
}
|
|
407
|
+
return eq(column, parsedTargetId[pk.fieldName]);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
return (await this.countRelatedRows(hop.parentCollection, hop.parentId, hop.relation, identity)) > 0;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Remove the junction row linking a parent to `targetId`, leaving the target
|
|
415
|
+
* row itself alone.
|
|
416
|
+
*
|
|
417
|
+
* This is what `DELETE authors/1/tags/5` has to mean for a many-to-many: the
|
|
418
|
+
* target is shared, so deleting the row would remove the tag from every other
|
|
419
|
+
* post that uses it. It used to do exactly that — resolve the path to the
|
|
420
|
+
* `tags` table and delete by primary key.
|
|
421
|
+
*/
|
|
422
|
+
async unlinkRelatedEntity(
|
|
423
|
+
tx: DrizzleClient,
|
|
424
|
+
hop: NestedPathHop,
|
|
425
|
+
targetId: string | number
|
|
426
|
+
): Promise<void> {
|
|
427
|
+
const through = hop.relation.through;
|
|
428
|
+
if (!through) {
|
|
429
|
+
throw new Error(`Relation '${hop.relationKey}' has no junction table to unlink through`);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const junctionTable = this.registry.getTable(through.table);
|
|
433
|
+
if (!junctionTable) {
|
|
434
|
+
throw new Error(`Junction table not found: ${through.table}`);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const sourceJunctionColumn = junctionTable[through.sourceColumn as keyof typeof junctionTable] as AnyPgColumn;
|
|
438
|
+
const targetJunctionColumn = junctionTable[through.targetColumn as keyof typeof junctionTable] as AnyPgColumn;
|
|
439
|
+
|
|
440
|
+
if (!sourceJunctionColumn || !targetJunctionColumn) {
|
|
441
|
+
throw new Error(`Junction columns not found for relation '${hop.relationKey}' on table '${through.table}'`);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const parentPks = requirePrimaryKeys(hop.parentCollection, this.registry);
|
|
445
|
+
const parsedParentId = parseIdValues(hop.parentId, parentPks)[parentPks[0].fieldName];
|
|
446
|
+
|
|
447
|
+
const targetPks = requirePrimaryKeys(hop.targetCollection, this.registry);
|
|
448
|
+
const parsedTargetId = parseIdValues(targetId, targetPks)[targetPks[0].fieldName];
|
|
449
|
+
|
|
450
|
+
await tx.delete(junctionTable).where(and(
|
|
451
|
+
eq(sourceJunctionColumn, parsedParentId),
|
|
452
|
+
eq(targetJunctionColumn, parsedTargetId)
|
|
453
|
+
));
|
|
454
|
+
|
|
455
|
+
logger.info(`Unlinked '${hop.relationKey}' ${parsedTargetId} from ${hop.parentCollection.slug} ${parsedParentId}`);
|
|
456
|
+
}
|
|
457
|
+
|
|
373
458
|
/**
|
|
374
459
|
* Batch fetch related rows for multiple parent rows to avoid N+1 queries
|
|
375
460
|
*/
|
|
@@ -1348,15 +1433,19 @@ parentSourceColName };
|
|
|
1348
1433
|
const parsedNewEntityIdObj = parseIdValues(newEntityId, targetPks);
|
|
1349
1434
|
const parsedNewEntityId = parsedNewEntityIdObj[targetIdInfo.fieldName];
|
|
1350
1435
|
|
|
1351
|
-
// Create the junction table entry linking parent to the
|
|
1436
|
+
// Create the junction table entry linking parent to the target row.
|
|
1352
1437
|
const junctionData = {
|
|
1353
1438
|
[sourceJunctionColumn.name]: parentId,
|
|
1354
1439
|
[targetJunctionColumn.name]: parsedNewEntityId
|
|
1355
1440
|
};
|
|
1356
1441
|
|
|
1357
|
-
|
|
1442
|
+
// Idempotent: a link either exists or it does not, so asking for one
|
|
1443
|
+
// twice is not an error. This is what lets `PUT parent/id/child/childId`
|
|
1444
|
+
// mean "this row belongs to this parent's set" — the only way to
|
|
1445
|
+
// attach an *existing* row, which previously had none.
|
|
1446
|
+
await tx.insert(junctionTable).values(junctionData).onConflictDoNothing();
|
|
1358
1447
|
|
|
1359
|
-
logger.info(`
|
|
1448
|
+
logger.info(`Linked '${relationKey}' ${parsedNewEntityId} to ${parentId}`);
|
|
1360
1449
|
} catch (error) {
|
|
1361
1450
|
logger.error(`Failed to create junction table entry for relation '${relationKey}'`, { error: error });
|
|
1362
1451
|
throw error;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { CollectionConfig, Relation } from "@rebasepro/types";
|
|
2
|
+
import { findRelation, resolveCollectionRelations } from "@rebasepro/common";
|
|
3
|
+
import { ApiError } from "@rebasepro/server";
|
|
4
|
+
|
|
5
|
+
import { getCollectionByPath } from "./collection-helpers";
|
|
6
|
+
import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The last hop of a nested collection path, e.g. `authors/1/posts`.
|
|
10
|
+
*
|
|
11
|
+
* The walk that produces this was written out four separate times — in
|
|
12
|
+
* `FetchService.fetchCollectionFromPath`, `FetchService.countEntitiesFromPath`,
|
|
13
|
+
* `PersistService.save` and `CollectionRegistry.getCollectionByPath` — and had
|
|
14
|
+
* drifted, so the read path and the write path did not agree on which relation
|
|
15
|
+
* a path named. It lives here once now.
|
|
16
|
+
*/
|
|
17
|
+
export interface NestedPathHop {
|
|
18
|
+
/** The collection the final relation is declared on (e.g. `authors`). */
|
|
19
|
+
parentCollection: CollectionConfig;
|
|
20
|
+
/** The parent's id as it appeared in the path, unparsed. */
|
|
21
|
+
parentId: string;
|
|
22
|
+
/** The path segment that named the relation (e.g. `posts`). */
|
|
23
|
+
relationKey: string;
|
|
24
|
+
relation: Relation;
|
|
25
|
+
/** `relation.target()`, resolved once. */
|
|
26
|
+
targetCollection: CollectionConfig;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* True when `path` addresses rows through a relation rather than a root
|
|
31
|
+
* collection.
|
|
32
|
+
*
|
|
33
|
+
* Any separator at all counts — a root collection slug never contains one — so
|
|
34
|
+
* a malformed path like `collection/id` is a *broken* nested path and gets
|
|
35
|
+
* reported as one by {@link resolveNestedPath}, rather than being looked up as
|
|
36
|
+
* a root collection whose slug happens to contain a slash.
|
|
37
|
+
*/
|
|
38
|
+
export function isNestedPath(path: string): boolean {
|
|
39
|
+
return path.includes("/");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function splitPathSegments(path: string): string[] {
|
|
43
|
+
return path.split("/").filter(s => s && s !== "undefined");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Walk a nested collection path down to the relation it ends in.
|
|
48
|
+
*
|
|
49
|
+
* Returns `undefined` for a plain root-collection path so callers can keep the
|
|
50
|
+
* root case on its existing code path. Throws when the path is malformed, or
|
|
51
|
+
* when a segment names a relation that does not exist — the same errors the
|
|
52
|
+
* individual walks used to raise, with the available names attached.
|
|
53
|
+
*/
|
|
54
|
+
export function resolveNestedPath(
|
|
55
|
+
path: string,
|
|
56
|
+
registry: PostgresCollectionRegistry
|
|
57
|
+
): NestedPathHop | undefined {
|
|
58
|
+
if (!isNestedPath(path)) return undefined;
|
|
59
|
+
|
|
60
|
+
const segments = splitPathSegments(path);
|
|
61
|
+
if (segments.length < 3 || segments.length % 2 === 0) {
|
|
62
|
+
throw new Error(`Invalid relation path: ${path}. Expected format: collection/id/relation`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let parentCollection = getCollectionByPath(segments[0], registry);
|
|
66
|
+
let parentId = segments[1];
|
|
67
|
+
|
|
68
|
+
for (let i = 2; i < segments.length; i += 2) {
|
|
69
|
+
const relationKey = segments[i];
|
|
70
|
+
const resolvedRelations = resolveCollectionRelations(parentCollection);
|
|
71
|
+
const relation = findRelation(resolvedRelations, relationKey);
|
|
72
|
+
|
|
73
|
+
if (!relation) {
|
|
74
|
+
const available = Object.keys(resolvedRelations).join(", ") || "(none)";
|
|
75
|
+
throw new Error(
|
|
76
|
+
`Relation '${relationKey}' not found in collection '${parentCollection.slug}'. Available relations: [${available}]`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const targetCollection = relation.target();
|
|
81
|
+
|
|
82
|
+
if (i === segments.length - 1) {
|
|
83
|
+
return {
|
|
84
|
+
parentCollection,
|
|
85
|
+
parentId,
|
|
86
|
+
relationKey,
|
|
87
|
+
relation,
|
|
88
|
+
targetCollection
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
parentCollection = targetCollection;
|
|
93
|
+
parentId = segments[i + 1];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Unreachable: the loop returns on the final segment, and the odd-length
|
|
97
|
+
// check above guarantees there is one.
|
|
98
|
+
throw new Error(`Unable to resolve path: ${path}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* A relation reached through a junction table — many-to-many, or a multi-hop
|
|
103
|
+
* `joinPath`. The target row is shared with other parents, so writing "through"
|
|
104
|
+
* such a path addresses the *link*, not the row.
|
|
105
|
+
*/
|
|
106
|
+
export function isJunctionBackedRelation(relation: Relation): boolean {
|
|
107
|
+
return Boolean(relation.through) || Boolean(relation.joinPath && relation.joinPath.length > 1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Reject a nested write whose final segment is a to-one relation.
|
|
112
|
+
*
|
|
113
|
+
* There is no column on the target row that records a to-one parent — the
|
|
114
|
+
* foreign key lives on the *parent* table. The write path used to fall through
|
|
115
|
+
* to `relation.localKey` here and stamp the parent's own FK column onto the
|
|
116
|
+
* target row, which either raised an opaque "column does not exist" or, when a
|
|
117
|
+
* column of that name happened to exist on the target, silently wrote the wrong
|
|
118
|
+
* one.
|
|
119
|
+
*/
|
|
120
|
+
export function assertWritableThrough(hop: NestedPathHop, path: string): void {
|
|
121
|
+
if (hop.relation.cardinality !== "many") {
|
|
122
|
+
throw ApiError.badRequest(
|
|
123
|
+
`"${path}" ends in the to-one relation '${hop.relationKey}', which cannot be written through: ` +
|
|
124
|
+
`the foreign key for a to-one relation lives on '${hop.parentCollection.slug}', not on ` +
|
|
125
|
+
`'${hop.targetCollection.slug}'. Write the target row at "${hop.targetCollection.slug}" and set ` +
|
|
126
|
+
`'${hop.relationKey}' on the parent instead.`,
|
|
127
|
+
"RELATION_NOT_WRITABLE"
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -38,6 +38,149 @@ export interface DrizzleDynamicQuery {
|
|
|
38
38
|
*/
|
|
39
39
|
export class DrizzleConditionBuilder {
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Express "reachable from this parent through this relation" as a plain
|
|
43
|
+
* `WHERE` condition on the target table.
|
|
44
|
+
*
|
|
45
|
+
* This is the primitive that lets a relation be a *filter* rather than an
|
|
46
|
+
* addressing scheme. A nested listing used to be served by its own query
|
|
47
|
+
* builder — `fetchEntitiesUsingJoins`, which grew joins the root pipeline
|
|
48
|
+
* did not have and lost the options the root pipeline did have (offset,
|
|
49
|
+
* filter, orderBy, include). Reduced to a condition, the same listing runs
|
|
50
|
+
* through the ordinary collection query, so it inherits all of them and
|
|
51
|
+
* there is one read path instead of two.
|
|
52
|
+
*
|
|
53
|
+
* The shapes:
|
|
54
|
+
* - inverse FK → `target.<fk> = :parentId`, a column comparison.
|
|
55
|
+
* - `through` → `EXISTS (SELECT 1 FROM junction …)`, correlated on the
|
|
56
|
+
* target's key, so the junction never multiplies rows the
|
|
57
|
+
* way an `INNER JOIN` would.
|
|
58
|
+
* - `joinPath` → the same `EXISTS`, with the path's steps joined inside
|
|
59
|
+
* it and the final step correlating to the outer row.
|
|
60
|
+
*/
|
|
61
|
+
static buildRelationScopeCondition(
|
|
62
|
+
relation: Relation,
|
|
63
|
+
/**
|
|
64
|
+
* Lazy: only `joinPath` and `localKey` relations need the parent's own
|
|
65
|
+
* table. An inverse foreign key and a junction are both expressible
|
|
66
|
+
* from the parent's *id* alone, and requiring the table for them would
|
|
67
|
+
* make a child listing fail on a parent whose table isn't registered.
|
|
68
|
+
*/
|
|
69
|
+
parent: () => { table: PgTable<any>; idColumn: AnyPgColumn },
|
|
70
|
+
parentId: string | number,
|
|
71
|
+
targetTable: PgTable<any>,
|
|
72
|
+
targetIdColumn: AnyPgColumn,
|
|
73
|
+
registry: PostgresCollectionRegistry
|
|
74
|
+
): SQL {
|
|
75
|
+
if (relation.joinPath && relation.joinPath.length > 0) {
|
|
76
|
+
const { table, idColumn } = parent();
|
|
77
|
+
return this.buildJoinPathScopeCondition(
|
|
78
|
+
relation.joinPath, table, idColumn, parentId, targetIdColumn, registry
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (relation.through) {
|
|
83
|
+
const junctionTable = registry.getTable(relation.through.table);
|
|
84
|
+
if (!junctionTable) {
|
|
85
|
+
throw new Error(`Junction table not found: ${relation.through.table}`);
|
|
86
|
+
}
|
|
87
|
+
const sourceCol = junctionTable[relation.through.sourceColumn as keyof typeof junctionTable] as AnyPgColumn;
|
|
88
|
+
const targetCol = junctionTable[relation.through.targetColumn as keyof typeof junctionTable] as AnyPgColumn;
|
|
89
|
+
if (!sourceCol || !targetCol) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`Junction columns '${relation.through.sourceColumn}'/'${relation.through.targetColumn}' not found in '${relation.through.table}'`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
return sql`EXISTS (SELECT 1 FROM ${junctionTable} WHERE ${targetCol} = ${targetIdColumn} AND ${sourceCol} = ${parentId})`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (relation.foreignKeyOnTarget) {
|
|
98
|
+
const fkColumn = targetTable[relation.foreignKeyOnTarget as keyof typeof targetTable] as AnyPgColumn;
|
|
99
|
+
if (!fkColumn) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`Foreign key column '${relation.foreignKeyOnTarget}' not found in the target table of relation ` +
|
|
102
|
+
`'${relation.relationName}'. A many-to-many relation needs \`through\` instead.`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
return eq(fkColumn, parentId);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (relation.localKey) {
|
|
109
|
+
// A to-one owning relation read as a scope: the single target row is
|
|
110
|
+
// the one the parent's foreign key points at.
|
|
111
|
+
const { table, idColumn } = parent();
|
|
112
|
+
return sql`${targetIdColumn} = (SELECT ${sql.identifier(relation.localKey)} FROM ${table} WHERE ${idColumn} = ${parentId})`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
throw new Error(
|
|
116
|
+
`Relation '${relation.relationName}' declares no \`foreignKeyOnTarget\`, \`through\`, \`joinPath\` or ` +
|
|
117
|
+
"`localKey`, so there is no way to tell which target rows belong to a parent."
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* `EXISTS` for an explicit `joinPath`.
|
|
123
|
+
*
|
|
124
|
+
* The path is declared source → target. The subquery replays every step but
|
|
125
|
+
* the last from inside, and turns the last one into the correlation with the
|
|
126
|
+
* outer target row — so the target table is never named twice and needs no
|
|
127
|
+
* alias. Each intermediate table is aliased positionally, which keeps a path
|
|
128
|
+
* that revisits a table (a self-referencing many-to-many) unambiguous.
|
|
129
|
+
*/
|
|
130
|
+
private static buildJoinPathScopeCondition(
|
|
131
|
+
joinPath: JoinStep[],
|
|
132
|
+
parentTable: PgTable<any>,
|
|
133
|
+
parentIdColumn: AnyPgColumn,
|
|
134
|
+
parentId: string | number,
|
|
135
|
+
targetIdColumn: AnyPgColumn,
|
|
136
|
+
registry: PostgresCollectionRegistry
|
|
137
|
+
): SQL {
|
|
138
|
+
const sourceAlias = "__rel_src";
|
|
139
|
+
const aliasFor = (index: number) => `__rel_j${index}`;
|
|
140
|
+
|
|
141
|
+
// Column reference against the previous hop: the aliased source for the
|
|
142
|
+
// first step, the previous aliased join table after that.
|
|
143
|
+
const fromRef = (stepIndex: number, column: string) =>
|
|
144
|
+
sql`${sql.identifier(stepIndex === 0 ? sourceAlias : aliasFor(stepIndex - 1))}.${sql.identifier(getColumnName(column))}`;
|
|
145
|
+
|
|
146
|
+
const pairs = (step: JoinStep): { from: string; to: string }[] => {
|
|
147
|
+
const from = Array.isArray(step.on.from) ? step.on.from : [step.on.from];
|
|
148
|
+
const to = Array.isArray(step.on.to) ? step.on.to : [step.on.to];
|
|
149
|
+
if (from.length !== to.length) {
|
|
150
|
+
throw new Error(`Join step on '${step.table}' has ${from.length} \`from\` columns and ${to.length} \`to\` columns`);
|
|
151
|
+
}
|
|
152
|
+
return from.map((f, i) => ({ from: f, to: to[i] }));
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const inner = joinPath.slice(0, -1);
|
|
156
|
+
const last = joinPath[joinPath.length - 1];
|
|
157
|
+
|
|
158
|
+
const joins: SQL[] = inner.map((step, index) => {
|
|
159
|
+
const table = registry.getTable(step.table);
|
|
160
|
+
if (!table) throw new Error(`Join table not found: ${step.table}`);
|
|
161
|
+
const on = pairs(step).map(({ from, to }) =>
|
|
162
|
+
sql`${fromRef(index, from)} = ${sql.identifier(aliasFor(index))}.${sql.identifier(getColumnName(to))}`
|
|
163
|
+
);
|
|
164
|
+
return sql`JOIN ${table} AS ${sql.identifier(aliasFor(index))} ON ${sql.join(on, sql` AND `)}`;
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// The last step correlates to the outer row instead of joining the
|
|
168
|
+
// target table into the subquery.
|
|
169
|
+
const lastPairs = pairs(last);
|
|
170
|
+
const correlation = lastPairs.length === 1
|
|
171
|
+
? sql`${fromRef(inner.length, lastPairs[0].from)} = ${targetIdColumn}`
|
|
172
|
+
: sql.join(
|
|
173
|
+
lastPairs.map(({ from, to }) =>
|
|
174
|
+
sql`${fromRef(inner.length, from)} = ${sql.identifier(getColumnName(to))}`
|
|
175
|
+
),
|
|
176
|
+
sql` AND `
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const joinsSql = joins.length > 0 ? sql` ${sql.join(joins, sql` `)}` : sql``;
|
|
180
|
+
|
|
181
|
+
return sql`EXISTS (SELECT 1 FROM ${parentTable} AS ${sql.identifier(sourceAlias)}${joinsSql} WHERE ${sql.identifier(sourceAlias)}.${sql.identifier(parentIdColumn.name)} = ${parentId} AND ${correlation})`;
|
|
182
|
+
}
|
|
183
|
+
|
|
41
184
|
/**
|
|
42
185
|
* Build filter conditions from FilterValues
|
|
43
186
|
*/
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ensure-collection-tables-CNlIONzj.js","names":[],"sources":["../src/schema/generate-postgres-ddl-logic.ts","../src/schema/ensure-collection-tables.ts"],"sourcesContent":["import { CollectionConfig, NumberProperty, Property, Relation, RelationProperty, SecurityOperation, SecurityRule, StringProperty, isPostgresCollectionConfig, DateProperty, ArrayProperty, MapProperty, ReferenceProperty, VectorProperty, BinaryProperty } from \"@rebasepro/types\";\nimport { getEnumVarName, getTableName, resolveCollectionRelations, findRelation, securityRuleToConditions, policyToPostgres, getEffectiveSecurityRules, getInjectedSecurityRules, resolveJunctionSpecs, getJunctionSecurityRules, getJunctionCollectionConfig } from \"@rebasepro/common\";\nimport { toSnakeCase, getPolicyNamesForRule } from \"@rebasepro/utils\";\n\n// --- Helper Functions ---\n\nexport const resolveColumnName = (propName: string, prop?: Property | null): string => {\n if (prop && \"columnName\" in prop && typeof prop.columnName === \"string\") {\n return prop.columnName;\n }\n return toSnakeCase(propName);\n};\n\nconst getPrimaryKeyProp = (collection: CollectionConfig): { name: string, type: \"string\" | \"number\", isUuid: boolean } => {\n if (collection.properties) {\n const idPropEntry = Object.entries(collection.properties).find(([_, prop]) => \"isId\" in (prop as unknown as object) && Boolean((prop as unknown as Record<string, unknown>).isId));\n if (idPropEntry) {\n const prop = idPropEntry[1] as unknown as Property;\n const isUuid = prop.type === \"string\" && \"isId\" in prop && (prop as unknown as StringProperty).isId === \"uuid\";\n return { name: idPropEntry[0], type: prop.type === \"number\" ? \"number\" : \"string\", isUuid };\n }\n }\n const idProp = collection.properties?.[\"id\"] as unknown as Property | undefined;\n if (idProp?.type === \"number\") {\n return { name: \"id\", type: \"number\", isUuid: false };\n }\n const isUuid = idProp?.type === \"string\" && \"isId\" in idProp && (idProp as unknown as StringProperty).isId === \"uuid\";\n return { name: \"id\", type: \"string\", isUuid: isUuid ?? false };\n};\n\nconst isNumericId = (collection: CollectionConfig): boolean => {\n return getPrimaryKeyProp(collection).type === \"number\";\n};\n\nconst getPrimaryKeyName = (collection: CollectionConfig): string => {\n return getPrimaryKeyProp(collection).name;\n};\n\nexport const isIdProperty = (propName: string, prop: Property, collection: CollectionConfig): boolean => {\n if (\"isId\" in prop && Boolean(prop.isId)) return true;\n const hasExplicitId = Object.values(collection.properties ?? {}).some(p => \"isId\" in (p as unknown as object) && Boolean((p as unknown as Record<string, unknown>).isId));\n return !hasExplicitId && propName === \"id\";\n};\n\n\ntype ResolveCollection = (slug: string) => CollectionConfig | undefined;\n\nconst generatePolicyDdl = (collection: CollectionConfig, rule: SecurityRule, resolveCollection: ResolveCollection): string => {\n const tableName = getTableName(collection);\n const ops: readonly SecurityOperation[] = rule.operations && rule.operations.length > 0\n ? rule.operations\n : [rule.operation ?? \"all\"];\n\n const policyNames = getPolicyNamesForRule(rule, tableName);\n\n return ops.map((op, opIdx) => {\n return generateSinglePolicyDdl(collection, rule, op, policyNames[opIdx], resolveCollection);\n }).join(\"\");\n};\n\nconst generateSinglePolicyDdl = (collection: CollectionConfig, rule: SecurityRule, operation: SecurityOperation, policyName: string, resolveCollection: ResolveCollection): string => {\n const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : \"public\";\n const tableName = getTableName(collection);\n const mode = (rule.mode ?? \"permissive\").toUpperCase();\n const operationUpper = operation.toUpperCase();\n const pgRoles = rule.pgRoles ? [...rule.pgRoles].sort() : [\"public\"];\n\n const needsUsing = operation !== \"insert\";\n const needsWithCheck = operation !== \"select\" && operation !== \"delete\";\n\n // Desugar the rule (access / ownerField / roles / structured condition / raw\n // SQL) into the shared PolicyExpression model, then compile to SQL. This is\n // the same normalization the client-side evaluator uses, so DDL and UI agree.\n const { usingExpr, withCheckExpr } = securityRuleToConditions(rule);\n\n let usingClause = needsUsing && usingExpr ? policyToPostgres(usingExpr, collection, { resolveCollection }) : null;\n let withCheckClause = needsWithCheck && withCheckExpr ? policyToPostgres(withCheckExpr, collection, { resolveCollection }) : null;\n\n if (!usingClause && needsUsing) {\n usingClause = \"false\";\n }\n if (!withCheckClause && needsWithCheck) {\n withCheckClause = \"false\";\n }\n\n let ddl = `DROP POLICY IF EXISTS \"${policyName}\" ON \"${schema}\".\"${tableName}\";\\n`;\n ddl += `CREATE POLICY \"${policyName}\" ON \"${schema}\".\"${tableName}\" AS ${mode} FOR ${operationUpper} TO ${pgRoles.map(r => `\"${r}\"`).join(\", \")}`;\n if (usingClause) ddl += ` USING (${usingClause})`;\n if (withCheckClause) ddl += ` WITH CHECK (${withCheckClause})`;\n return `${ddl};\\n`;\n};\n\nexport const getSqlColumnType = (propName: string, prop: Property, collection: CollectionConfig, collections: CollectionConfig[]): string => {\n switch (prop.type) {\n case \"string\": {\n const stringProp = prop as StringProperty;\n if (stringProp.enum) {\n const tableName = getTableName(collection);\n const colName = resolveColumnName(propName, prop);\n const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : \"public\";\n return `\"${schema}\".\"${tableName}_${colName}\"`;\n }\n if (stringProp.isId === \"uuid\" || stringProp.columnType === \"uuid\") {\n return \"UUID\";\n }\n if (stringProp.columnType === \"text\" || stringProp.ui?.markdown || stringProp.ui?.multiline) {\n return \"TEXT\";\n }\n if (stringProp.columnType === \"char\") {\n return \"CHAR(255)\";\n }\n return \"VARCHAR(255)\";\n }\n case \"number\": {\n const numProp = prop as NumberProperty;\n const isId = isIdProperty(propName, prop, collection);\n if (\"isId\" in numProp && numProp.isId === \"increment\") {\n return \"INTEGER GENERATED BY DEFAULT AS IDENTITY\";\n }\n if (numProp.columnType) {\n if (numProp.columnType === \"double precision\") return \"DOUBLE PRECISION\";\n return numProp.columnType.toUpperCase();\n }\n return (numProp.validation?.integer || isId) ? \"INTEGER\" : \"NUMERIC\";\n }\n case \"boolean\":\n return \"BOOLEAN\";\n case \"date\": {\n const dateProp = prop as DateProperty;\n if (dateProp.columnType === \"date\") return \"DATE\";\n if (dateProp.columnType === \"time\") return \"TIME\";\n return \"TIMESTAMP WITH TIME ZONE\";\n }\n case \"map\": {\n const mapProp = prop as MapProperty;\n return mapProp.columnType === \"json\" ? \"JSON\" : \"JSONB\";\n }\n case \"array\": {\n const arrayProp = prop as ArrayProperty;\n let colType = arrayProp.columnType;\n if (!colType && arrayProp.of && !Array.isArray(arrayProp.of)) {\n const ofProp = arrayProp.of as Property;\n if (ofProp.type === \"string\") {\n colType = \"text[]\";\n } else if (ofProp.type === \"number\") {\n colType = ofProp.validation?.integer ? \"integer[]\" : \"numeric[]\";\n } else if (ofProp.type === \"boolean\") {\n colType = \"boolean[]\";\n }\n }\n if (colType === \"json\") return \"JSON\";\n if (colType === \"text[]\") return \"TEXT[]\";\n if (colType === \"integer[]\") return \"INTEGER[]\";\n if (colType === \"boolean[]\") return \"BOOLEAN[]\";\n if (colType === \"numeric[]\") return \"NUMERIC[]\";\n return \"JSONB\";\n }\n case \"vector\": {\n const vp = prop as VectorProperty;\n return `VECTOR(${vp.dimensions})`;\n }\n case \"binary\": {\n return \"BYTEA\";\n }\n case \"relation\": {\n const refProp = prop as RelationProperty;\n const resolvedRelations = resolveCollectionRelations(collection);\n const relation = findRelation(resolvedRelations, refProp.relationName ?? propName);\n if (!relation || relation.direction !== \"owning\" || relation.cardinality !== \"one\") {\n throw new Error(`Relation ${propName} is not an owning one-to-one/many-to-one relation`);\n }\n let targetCollection: CollectionConfig;\n try {\n targetCollection = relation.target();\n } catch {\n return \"VARCHAR(255)\";\n }\n const pkProp = getPrimaryKeyProp(targetCollection);\n return pkProp.type === \"number\" ? \"INTEGER\" : (pkProp.isUuid ? \"UUID\" : \"VARCHAR(255)\");\n }\n case \"reference\": {\n const refProp = prop as ReferenceProperty;\n const targetCollection = collections.find(c => c.slug === refProp.path || getTableName(c) === refProp.path);\n if (!targetCollection) return \"VARCHAR(255)\";\n const pkProp = getPrimaryKeyProp(targetCollection);\n return pkProp.type === \"number\" ? \"INTEGER\" : (pkProp.isUuid ? \"UUID\" : \"VARCHAR(255)\");\n }\n default:\n return \"VARCHAR(255)\";\n }\n};\n\nexport const generatePostgresDdl = async (\n collections: CollectionConfig[],\n options: { includePolicies?: boolean } = { includePolicies: true }\n): Promise<string> => {\n let ddl = \"-- This file is auto-generated by the Rebase DDL generator. Do not edit manually.\\n\\n\";\n\n // 1. Create custom schemas\n const uniqueSchemas = Array.from(new Set([\n \"auth\",\n ...collections.map(c => isPostgresCollectionConfig(c) ? c.schema : undefined).filter(Boolean)\n ]));\n uniqueSchemas.forEach(schema => {\n if (schema) ddl += `CREATE SCHEMA IF NOT EXISTS \"${schema}\";\\n`;\n });\n if (uniqueSchemas.length > 0) ddl += \"\\n\";\n\n // 2. Generate Enums\n collections.forEach(collection => {\n const collectionTable = getTableName(collection);\n const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : \"public\";\n Object.entries(collection.properties ?? {}).forEach(([propName, prop]) => {\n if ((\"enum\" in prop) && (prop.type === \"string\" || prop.type === \"number\") && prop.enum) {\n const enumDbName = `${collectionTable}_${resolveColumnName(propName, prop)}`;\n const values = Array.isArray(prop.enum)\n ? (prop.enum as (string | number | { id: string | number })[]).map((v: string | number | { id: string | number }) =>\n String(typeof v === \"object\" && v !== null && \"id\" in v ? v.id : v)\n )\n : Object.keys(prop.enum);\n if (values.length > 0) {\n ddl += `CREATE TYPE \"${schema}\".\"${enumDbName}\" AS ENUM (${values.map(v => `'${v}'`).join(\", \")});\\n`;\n }\n }\n });\n });\n if (ddl.endsWith(\";\\n\")) ddl += \"\\n\";\n\n // Junction policy derivation needs every declaring side of each junction,\n // not just the first relation that reached it in the walk below.\n const junctionSpecs = resolveJunctionSpecs(collections);\n\n const allTablesToGenerate = new Map<string, {\n collection: CollectionConfig,\n isJunction?: boolean,\n relation?: Relation,\n sourceCollection?: CollectionConfig\n }>();\n\n // Identify all tables\n for (const collection of collections) {\n const tableName = getTableName(collection);\n if (tableName) {\n allTablesToGenerate.set(tableName, { collection });\n }\n\n const resolvedRelations = resolveCollectionRelations(collection);\n for (const relation of Object.values(resolvedRelations)) {\n if (relation.through) {\n const junctionTableName = relation.through.table;\n if (!allTablesToGenerate.has(junctionTableName)) {\n allTablesToGenerate.set(junctionTableName, {\n collection: {\n table: junctionTableName,\n properties: {}\n } as CollectionConfig,\n isJunction: true,\n relation: relation,\n sourceCollection: collection\n });\n }\n }\n }\n }\n\n // 3. Generate tables\n const fkStatements: string[] = [];\n // Policies are emitted after every CREATE TABLE, like the FK constraints:\n // a policy may reference other tables (a junction's derived policies always\n // reference both endpoints; `policy.existsIn` references a join table), and\n // CREATE POLICY validates those relations at creation time.\n const policyStatements: string[] = [];\n for (const [tableName, {\n collection,\n isJunction,\n relation,\n sourceCollection\n }] of allTablesToGenerate.entries()) {\n const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : \"public\";\n const baseTableName = tableName.includes(\".\") ? tableName.split(\".\").pop()! : tableName;\n\n if (isJunction && relation && sourceCollection && relation.through) {\n const targetCollection = relation.target();\n const sourceTable = getTableName(sourceCollection);\n const targetTable = getTableName(targetCollection);\n const sourceSchema = isPostgresCollectionConfig(sourceCollection) && sourceCollection.schema ? sourceCollection.schema : \"public\";\n const targetSchema = isPostgresCollectionConfig(targetCollection) && targetCollection.schema ? targetCollection.schema : \"public\";\n const { sourceColumn, targetColumn } = relation.through;\n\n const sourceColType = isNumericId(sourceCollection) ? \"INTEGER\" : (getPrimaryKeyProp(sourceCollection).isUuid ? \"UUID\" : \"VARCHAR(255)\");\n const targetColType = isNumericId(targetCollection) ? \"INTEGER\" : (getPrimaryKeyProp(targetCollection).isUuid ? \"UUID\" : \"VARCHAR(255)\");\n const sourceId = getPrimaryKeyName(sourceCollection);\n const targetId = getPrimaryKeyName(targetCollection);\n\n const onDelete = relation.onDelete ?? \"CASCADE\";\n\n ddl += `CREATE TABLE \"${schema}\".\"${baseTableName}\" (\\n`;\n ddl += ` \"${sourceColumn}\" ${sourceColType} NOT NULL,\\n`;\n ddl += ` \"${targetColumn}\" ${targetColType} NOT NULL,\\n`;\n ddl += ` PRIMARY KEY (\"${sourceColumn}\", \"${targetColumn}\")\\n`;\n ddl += `);\\n\\n`;\n\n fkStatements.push(`ALTER TABLE \"${schema}\".\"${baseTableName}\" ADD CONSTRAINT \"${baseTableName}_${sourceColumn}_fkey\" FOREIGN KEY (\"${sourceColumn}\") REFERENCES \"${sourceSchema}\".\"${sourceTable}\" (\"${sourceId}\") ON DELETE ${onDelete.toUpperCase()};`);\n fkStatements.push(`ALTER TABLE \"${schema}\".\"${baseTableName}\" ADD CONSTRAINT \"${baseTableName}_${targetColumn}_fkey\" FOREIGN KEY (\"${targetColumn}\") REFERENCES \"${targetSchema}\".\"${targetTable}\" (\"${targetId}\") ON DELETE ${onDelete.toUpperCase()};`);\n\n if (options.includePolicies) {\n // Junction tables are generated tables like any other: locked by\n // default, with derived policies — reads follow the endpoints'\n // visibility, writes follow the declaring side's update rules.\n // Without this they were the one kind of generated table with no\n // RLS at all, readable and writable by every signed-in user.\n ddl += `ALTER TABLE \"${schema}\".\"${baseTableName}\" ENABLE ROW LEVEL SECURITY;\\n`;\n ddl += `\\n`;\n\n const spec = junctionSpecs.get(baseTableName);\n if (spec) {\n const junctionCollection = getJunctionCollectionConfig(spec);\n const resolveCollection: ResolveCollection = (slug) => collections.find(c => c.slug === slug || getTableName(c) === slug);\n getJunctionSecurityRules(spec).forEach((rule: SecurityRule) => {\n policyStatements.push(generatePolicyDdl(junctionCollection, rule, resolveCollection));\n });\n }\n }\n } else if (!isJunction) {\n ddl += `CREATE TABLE \"${schema}\".\"${baseTableName}\" (\\n`;\n const columns: string[] = [];\n\n Object.entries(collection.properties ?? {}).forEach(([propName, prop]) => {\n if (prop.type === \"relation\") {\n const refProp = prop as RelationProperty;\n const resolvedRelations = resolveCollectionRelations(collection);\n const relInfo = findRelation(resolvedRelations, refProp.relationName ?? propName);\n\n if (!relInfo || relInfo.direction !== \"owning\" || relInfo.cardinality !== \"one\" || !relInfo.localKey) {\n return;\n }\n\n if (collection.properties[relInfo.localKey] && propName !== relInfo.localKey) {\n return;\n }\n\n let targetCollection: CollectionConfig;\n try {\n targetCollection = relInfo.target();\n } catch {\n return;\n }\n\n const targetTable = getTableName(targetCollection);\n const targetSchema = isPostgresCollectionConfig(targetCollection) && targetCollection.schema ? targetCollection.schema : \"public\";\n const targetId = getPrimaryKeyName(targetCollection);\n const fkColType = getSqlColumnType(propName, prop, collection, collections);\n \n const onUpdate = relInfo.onUpdate ? ` ON UPDATE ${relInfo.onUpdate.toUpperCase()}` : \"\";\n const required = prop.validation?.required;\n const onDeleteVal = relInfo.onDelete ?? (required ? \"CASCADE\" : \"SET NULL\");\n \n let colDef = ` \"${relInfo.localKey}\" ${fkColType}`;\n if (required) colDef += \" NOT NULL\";\n columns.push(colDef);\n\n fkStatements.push(`ALTER TABLE \"${schema}\".\"${baseTableName}\" ADD CONSTRAINT \"${baseTableName}_${relInfo.localKey}_fkey\" FOREIGN KEY (\"${relInfo.localKey}\") REFERENCES \"${targetSchema}\".\"${targetTable}\" (\"${targetId}\") ON DELETE ${onDeleteVal.toUpperCase()}${onUpdate};`);\n } else if (prop.type === \"reference\") {\n const refProp = prop as ReferenceProperty;\n const targetCollection = collections.find(c => c.slug === refProp.path || getTableName(c) === refProp.path);\n const colName = resolveColumnName(propName, prop);\n const colType = getSqlColumnType(propName, prop, collection, collections);\n const required = prop.validation?.required;\n\n if (!targetCollection) {\n let colDef = ` \"${colName}\" ${colType}`;\n if (required) colDef += \" NOT NULL\";\n columns.push(colDef);\n } else {\n const targetTable = getTableName(targetCollection);\n const targetSchema = isPostgresCollectionConfig(targetCollection) && targetCollection.schema ? targetCollection.schema : \"public\";\n const targetId = getPrimaryKeyName(targetCollection);\n const onDelete = required ? \"CASCADE\" : \"SET NULL\";\n\n let colDef = ` \"${colName}\" ${colType}`;\n if (required) colDef += \" NOT NULL\";\n columns.push(colDef);\n\n fkStatements.push(`ALTER TABLE \"${schema}\".\"${baseTableName}\" ADD CONSTRAINT \"${baseTableName}_${colName}_fkey\" FOREIGN KEY (\"${colName}\") REFERENCES \"${targetSchema}\".\"${targetTable}\" (\"${targetId}\") ON DELETE ${onDelete.toUpperCase()};`);\n }\n } else {\n const colName = resolveColumnName(propName, prop);\n const colType = getSqlColumnType(propName, prop, collection, collections);\n let colDef = ` \"${colName}\" ${colType}`;\n\n if (isIdProperty(propName, prop, collection)) {\n colDef += \" PRIMARY KEY\";\n }\n\n if (\"isId\" in prop && prop.isId !== \"manual\" && prop.isId !== true && prop.isId !== \"increment\") {\n if (prop.isId === \"uuid\") {\n colDef += \" DEFAULT gen_random_uuid()\";\n } else if (prop.isId === \"cuid\") {\n colDef += \" DEFAULT cuid()\";\n } else if (typeof prop.isId === \"string\") {\n colDef += ` DEFAULT ${prop.isId}`;\n }\n }\n\n if (!isIdProperty(propName, prop, collection) && prop.validation?.unique) {\n colDef += \" UNIQUE\";\n }\n\n if (prop.type === \"date\") {\n const dateProp = prop as DateProperty;\n if (dateProp.autoValue === \"on_create\" || dateProp.autoValue === \"on_update\") {\n colDef += \" DEFAULT now()\";\n }\n }\n\n if (prop.validation?.required && !colDef.includes(\"PRIMARY KEY\")) {\n colDef += \" NOT NULL\";\n }\n\n columns.push(colDef);\n }\n });\n\n // Backwards compatibility: add default id primary key if missing\n const hasPk = columns.some(c => c.includes(\"PRIMARY KEY\"));\n if (!hasPk) {\n columns.unshift(' \"id\" VARCHAR(255) PRIMARY KEY');\n }\n\n ddl += columns.join(\",\\n\");\n ddl += `\\n);\\n\\n`;\n\n if (options.includePolicies) {\n // Enable RLS and add Policies. No FORCE: authenticated requests\n // run as the non-owner `rebase_user` role, which plain ENABLE\n // already binds. The owner (server context) must bypass — it is\n // the trusted plane (auth flows, dataAsAdmin).\n ddl += `ALTER TABLE \"${schema}\".\"${baseTableName}\" ENABLE ROW LEVEL SECURITY;\\n`;\n ddl += `\\n`;\n\n const securityRules = getEffectiveSecurityRules(collection);\n if (securityRules.length > 0) {\n const resolveCollection: ResolveCollection = (slug) => collections.find(c => c.slug === slug || getTableName(c) === slug);\n securityRules.forEach((rule: SecurityRule) => {\n policyStatements.push(generatePolicyDdl(collection, rule, resolveCollection));\n });\n }\n }\n }\n }\n\n if (fkStatements.length > 0) {\n ddl += \"-- Foreign Key Constraints\\n\";\n ddl += fkStatements.join(\"\\n\") + \"\\n\\n\";\n }\n\n if (policyStatements.length > 0) {\n ddl += \"-- Row Level Security Policies\\n\";\n ddl += policyStatements.join(\"\");\n ddl += \"\\n\";\n }\n\n return ddl;\n};\n\nexport const generatePostgresPoliciesDdl = (collections: CollectionConfig[]): string => {\n let ddl = \"-- This file contains RLS policies generated by Rebase. Applied separately from migrations.\\n\\n\";\n\n const allTablesToGenerate = new Map<string, {\n collection: CollectionConfig\n }>();\n\n for (const collection of collections) {\n const tableName = getTableName(collection);\n if (tableName) {\n allTablesToGenerate.set(tableName, { collection });\n }\n }\n\n for (const [tableName, { collection }] of allTablesToGenerate.entries()) {\n const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : \"public\";\n const baseTableName = tableName.includes(\".\") ? tableName.split(\".\").pop()! : tableName;\n\n // No FORCE: user requests run as the non-owner `rebase_user` role\n // (plain ENABLE binds them); the owner is the trusted server context.\n ddl += `ALTER TABLE \"${schema}\".\"${baseTableName}\" ENABLE ROW LEVEL SECURITY;\\n`;\n ddl += `\\n`;\n\n const securityRules = getEffectiveSecurityRules(collection);\n if (securityRules.length > 0) {\n const resolveCollection: ResolveCollection = (slug) => collections.find(c => c.slug === slug || getTableName(c) === slug);\n const injectedNames = new Set(getInjectedSecurityRules(collection).map((rule) => rule.name));\n\n securityRules.forEach((rule: SecurityRule) => {\n // Say which policies the author did not write. They are permissive,\n // so they OR with the declared rules and widen the final ACL beyond\n // what `securityRules` reads like — and re-appear after any manual\n // DROP, because a push asserts the declared state.\n if (rule.name && injectedNames.has(rule.name)) {\n ddl += `-- Injected by Rebase (not from this collection's securityRules).\\n`;\n ddl += `-- Set \\`disableDefaultPolicies: true\\` on \"${collection.slug}\" to drop these and own its RLS outright.\\n`;\n }\n ddl += generatePolicyDdl(collection, rule, resolveCollection);\n });\n ddl += \"\\n\";\n }\n }\n\n // Junction tables are generated from `through` relations, not declared as\n // collections, so the walk above never sees them. They get the same\n // treatment as any generated table: locked by default, with derived\n // policies — reads follow the endpoints, writes follow the declaring\n // side's update rules.\n const junctionSpecs = resolveJunctionSpecs(collections);\n for (const spec of junctionSpecs.values()) {\n ddl += `ALTER TABLE \"${spec.schema}\".\"${spec.table}\" ENABLE ROW LEVEL SECURITY;\\n`;\n ddl += `\\n`;\n\n const junctionRules = getJunctionSecurityRules(spec);\n if (junctionRules.length === 0) continue;\n\n const junctionCollection = getJunctionCollectionConfig(spec);\n const resolveCollection: ResolveCollection = (slug) => collections.find(c => c.slug === slug || getTableName(c) === slug);\n const declaringSlugs = spec.declaringSides.map(s => s.collection.slug).join('\", \"');\n\n ddl += `-- Derived by Rebase for the junction \"${spec.table}\" (no collection declares it).\\n`;\n ddl += `-- Reads require both endpoint rows to be visible; writes follow the update\\n`;\n ddl += `-- rules of \"${declaringSlugs}\". Set \\`disableDefaultPolicies: true\\` on the\\n`;\n ddl += `-- declaring collection(s) to drop these and police the junction yourself.\\n`;\n junctionRules.forEach((rule: SecurityRule) => {\n ddl += generatePolicyDdl(junctionCollection, rule, resolveCollection);\n });\n ddl += \"\\n\";\n }\n\n return ddl;\n};\n\n","/**\n * Bringing a database up to date with a bundle's collections, additively.\n *\n * ## Why this exists\n *\n * A managed runtime boots someone else's compiled project against a database it\n * has never seen. Auth tables are ensured at boot already, but collection tables\n * were not created by anything: the platform ran the app and every `/api/data/*`\n * request answered 500 on a missing relation. `rebase db push` cannot help — it\n * is an Atlas-driven CLI command, and the runtime image ships no CLI.\n *\n * ## Why additive-only, forever\n *\n * This runs unattended, against a database with customers' data in it, with no\n * human reading a diff. So it may only ever do things that cannot lose data:\n * create a missing table, add a missing column, create a missing enum type.\n *\n * It will **never** drop a table or a column, narrow a type, or alter a\n * constraint. A removed field leaves its column behind; a renamed field looks\n * like an addition and the old column stays. That is the correct trade for an\n * automated path — the alternative is an unattended process that can silently\n * destroy a column, which is precisely the failure `db push` was hardened\n * against. Destructive changes stay a deliberate, human-reviewed migration.\n *\n * Because of that, this is safe to run on every boot, and re-running it is a\n * no-op.\n */\nimport { type CollectionConfig, type Property, isPostgresCollectionConfig } from \"@rebasepro/types\";\nimport { getTableName } from \"@rebasepro/common\";\nimport {\n getSqlColumnType,\n resolveColumnName,\n isIdProperty\n} from \"./generate-postgres-ddl-logic\";\n\n/**\n * The subset of a database handle this needs: run a statement, get rows back.\n *\n * Deliberately parameterless. Everything here is DDL or catalogue reads keyed by\n * schema name, and schema names are identifiers — they cannot be bound as\n * parameters anyway. They are validated against {@link SAFE_IDENTIFIER} before\n * they reach a statement, so a config that somehow carried a quote is refused\n * rather than concatenated.\n */\nexport interface Queryable {\n query<T = unknown>(sql: string): Promise<{ rows: T[] }>;\n}\n\n/** Postgres identifiers this module is willing to interpolate. */\nconst SAFE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_$]*$/;\n\nfunction assertSafeIdentifier(value: string, what: string): string {\n if (!SAFE_IDENTIFIER.test(value)) {\n throw new Error(`Refusing to build SQL with an unsafe ${what}: ${JSON.stringify(value)}`);\n }\n return value;\n}\n\n/** What the database currently has, as the planner needs it. */\nexport interface ExistingSchema {\n /** `schema.table` → set of column names. */\n tables: Map<string, Set<string>>;\n /** `schema.typename` of every enum type that already exists. */\n enums: Set<string>;\n}\n\nexport interface EnsureAction {\n kind: \"create-enum\" | \"create-table\" | \"add-column\";\n /** Qualified target, for logging: `public.posts` or `public.posts.title`. */\n target: string;\n sql: string;\n}\n\nexport interface EnsurePlan {\n actions: EnsureAction[];\n /** Every statement, in dependency order. Empty when the schema is current. */\n statements: string[];\n}\n\nfunction schemaOf(collection: CollectionConfig): string {\n return isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : \"public\";\n}\n\nfunction qualified(collection: CollectionConfig): string {\n return `${schemaOf(collection)}.${getTableName(collection)}`;\n}\n\n/**\n * Enum types a collection's properties require, as `schema.typename`.\n *\n * Named exactly as the DDL generator names them (`<table>_<column>`), because\n * a column added here has to reference the same type the generator would have\n * created — a second, differently-named type for the same field would be a\n * silent schema fork.\n */\nfunction requiredEnums(collection: CollectionConfig): { name: string; values: string[] }[] {\n const table = getTableName(collection);\n const schema = schemaOf(collection);\n const out: { name: string; values: string[] }[] = [];\n for (const [propName, prop] of Object.entries(collection.properties ?? {})) {\n const p = prop as Property;\n if (!(\"enum\" in p) || !p.enum) continue;\n if (p.type !== \"string\" && p.type !== \"number\") continue;\n const values = (p.enum as unknown[])\n .map(entry =>\n entry && typeof entry === \"object\" && \"id\" in (entry as Record<string, unknown>)\n ? String((entry as Record<string, unknown>).id)\n : String(entry)\n )\n .filter(v => v.length > 0);\n if (values.length === 0) continue;\n out.push({ name: `${schema}.${table}_${resolveColumnName(propName, p)}`, values });\n }\n return out;\n}\n\n/** Single-quote escaping for an enum label. */\nfunction quoteLiteral(value: string): string {\n return `'${value.replace(/'/g, \"''\")}'`;\n}\n\n/**\n * Decide what to add. Pure — the caller supplies what exists and runs the result.\n *\n * Ordering matters and is deliberate: enum types before the tables and columns\n * that reference them, tables before the columns added to other tables (a new\n * table may be the target of a relation), and nothing is emitted twice.\n */\nexport function planCollectionSchemaEnsure(\n collections: CollectionConfig[],\n existing: ExistingSchema\n): EnsurePlan {\n const actions: EnsureAction[] = [];\n const plannedEnums = new Set<string>();\n\n // 1. Enum types. `CREATE TYPE` has no IF NOT EXISTS, so an existing type is\n // skipped by name rather than guarded in SQL.\n for (const collection of collections) {\n for (const { name, values } of requiredEnums(collection)) {\n if (existing.enums.has(name) || plannedEnums.has(name)) continue;\n plannedEnums.add(name);\n const [schema, typeName] = name.split(\".\");\n actions.push({\n kind: \"create-enum\",\n target: name,\n sql: `CREATE TYPE \"${schema}\".\"${typeName}\" AS ENUM (${values.map(quoteLiteral).join(\", \")});`\n });\n }\n }\n\n // 2. Missing tables. Only the identity column is created here; every other\n // column is added by step 3, so a new table and an existing table that\n // gained a field travel the exact same code path. One way to build a\n // column means one way for it to be wrong.\n const created = new Set<string>();\n for (const collection of collections) {\n const key = qualified(collection);\n if (existing.tables.has(key) || created.has(key)) continue;\n created.add(key);\n const schema = schemaOf(collection);\n const table = getTableName(collection);\n const idEntry = Object.entries(collection.properties ?? {}).find(([n, p]) =>\n isIdProperty(n, p as Property, collection)\n );\n const idName = idEntry ? resolveColumnName(idEntry[0], idEntry[1] as Property) : \"id\";\n const idProp = idEntry?.[1] as Property | undefined;\n let idDef: string;\n if (idProp?.type === \"number\") {\n idDef = `\"${idName}\" BIGSERIAL PRIMARY KEY`;\n } else if (\n idProp &&\n idProp.type === \"string\" &&\n (idProp as { isId?: unknown }).isId === \"uuid\"\n ) {\n idDef = `\"${idName}\" UUID PRIMARY KEY DEFAULT gen_random_uuid()`;\n } else {\n idDef = `\"${idName}\" TEXT PRIMARY KEY`;\n }\n actions.push({\n kind: \"create-table\",\n target: key,\n sql: `CREATE TABLE IF NOT EXISTS \"${schema}\".\"${table}\" (${idDef});`\n });\n }\n\n // 3. Missing columns, on both brand-new and pre-existing tables.\n for (const collection of collections) {\n const key = qualified(collection);\n const schema = schemaOf(collection);\n const table = getTableName(collection);\n const present = existing.tables.get(key) ?? new Set<string>();\n for (const [propName, prop] of Object.entries(collection.properties ?? {})) {\n const p = prop as Property;\n if (isIdProperty(propName, p, collection)) continue;\n // A relation's own column is emitted by the DDL generator with a\n // foreign key; adding a bare column here would create the column\n // without the constraint and make the generator's later output\n // disagree with the database. Left to a real migration.\n if (p.type === \"reference\" || p.type === \"relation\") continue;\n const column = resolveColumnName(propName, p);\n if (present.has(column)) continue;\n const type = getSqlColumnType(propName, p, collection, collections);\n actions.push({\n kind: \"add-column\",\n target: `${key}.${column}`,\n // Never NOT NULL: an existing table with rows cannot take a\n // non-null column without a default, and inventing one would be\n // guessing at the customer's data.\n sql: `ALTER TABLE \"${schema}\".\"${table}\" ADD COLUMN IF NOT EXISTS \"${column}\" ${type};`\n });\n }\n }\n\n return { actions, statements: actions.map(a => a.sql) };\n}\n\n/** Read what the database has, for the schemas the collections live in. */\nexport async function readExistingSchema(\n client: Queryable,\n schemas: string[]\n): Promise<ExistingSchema> {\n const tables = new Map<string, Set<string>>();\n const enums = new Set<string>();\n if (schemas.length === 0) return { tables, enums };\n\n const inList = schemas\n .map(schema => `'${assertSafeIdentifier(schema, \"schema name\")}'`)\n .join(\", \");\n\n const { rows: columns } = await client.query<{\n table_schema: string;\n table_name: string;\n column_name: string;\n }>(\n `SELECT table_schema, table_name, column_name\n FROM information_schema.columns\n WHERE table_schema IN (${inList})`\n );\n for (const row of columns) {\n const key = `${row.table_schema}.${row.table_name}`;\n if (!tables.has(key)) tables.set(key, new Set());\n tables.get(key)!.add(row.column_name);\n }\n\n const { rows: enumRows } = await client.query<{ schema: string; name: string }>(\n `SELECT n.nspname AS schema, t.typname AS name\n FROM pg_type t\n JOIN pg_namespace n ON t.typnamespace = n.oid\n WHERE t.typtype = 'e' AND n.nspname IN (${inList})`\n );\n for (const row of enumRows) enums.add(`${row.schema}.${row.name}`);\n\n return { tables, enums };\n}\n\n/**\n * Bring the database up to date. Returns what it did.\n *\n * Each statement runs on its own rather than in one transaction: they are all\n * independently safe and idempotent, and a single failure (an enum label that\n * cannot be added, say) should not roll back the tables that were created fine.\n * The error is surfaced with the statement that caused it.\n */\nexport async function ensureCollectionTables(\n client: Queryable,\n collections: CollectionConfig[],\n log?: (message: string) => void\n): Promise<EnsurePlan> {\n const schemas = Array.from(new Set(collections.map(schemaOf)));\n for (const schema of schemas) {\n assertSafeIdentifier(schema, \"schema name\");\n if (schema !== \"public\") {\n await client.query(`CREATE SCHEMA IF NOT EXISTS \"${schema}\";`);\n }\n }\n\n const existing = await readExistingSchema(client, schemas);\n const plan = planCollectionSchemaEnsure(collections, existing);\n\n if (plan.actions.length === 0) {\n log?.(\"Schema is up to date; nothing to create.\");\n return plan;\n }\n\n for (const action of plan.actions) {\n try {\n await client.query(action.sql);\n log?.(`${action.kind}: ${action.target}`);\n } catch (err) {\n throw new Error(\n `Failed to ${action.kind} ${action.target}: ` +\n `${err instanceof Error ? err.message : String(err)}\\n ${action.sql}`\n );\n }\n }\n return plan;\n}\n"],"mappings":";;;;;;AAMA,IAAa,qBAAqB,UAAkB,SAAmC;CACnF,IAAI,QAAQ,gBAAgB,QAAQ,OAAO,KAAK,eAAe,UAC3D,OAAO,KAAK;CAEhB,OAAO,YAAY,QAAQ;AAC/B;AAEA,IAAM,qBAAqB,eAA+F;CACtH,IAAI,WAAW,YAAY;EACvB,MAAM,cAAc,OAAO,QAAQ,WAAW,UAAU,EAAE,MAAM,CAAC,GAAG,UAAU,UAAW,QAA8B,QAAS,KAA4C,IAAI,CAAC;EACjL,IAAI,aAAa;GACb,MAAM,OAAO,YAAY;GACzB,MAAM,SAAS,KAAK,SAAS,YAAY,UAAU,QAAS,KAAmC,SAAS;GACxG,OAAO;IAAE,MAAM,YAAY;IAAI,MAAM,KAAK,SAAS,WAAW,WAAW;IAAU;GAAO;EAC9F;CACJ;CACA,MAAM,SAAS,WAAW,aAAa;CACvC,IAAI,QAAQ,SAAS,UACjB,OAAO;EAAE,MAAM;EAAM,MAAM;EAAU,QAAQ;CAAM;CAGvD,OAAO;EAAE,MAAM;EAAM,MAAM;EAAU,QADtB,QAAQ,SAAS,YAAY,UAAU,UAAW,OAAqC,SAAS;CAClD;AACjE;AAUA,IAAa,gBAAgB,UAAkB,MAAgB,eAA0C;CACrG,IAAI,UAAU,QAAQ,QAAQ,KAAK,IAAI,GAAG,OAAO;CAEjD,OAAO,CADe,OAAO,OAAO,WAAW,cAAc,CAAC,CAAC,EAAE,MAAK,MAAK,UAAW,KAA2B,QAAS,EAAyC,IAAI,CAC/J,KAAiB,aAAa;AAC1C;AAkDA,IAAa,oBAAoB,UAAkB,MAAgB,YAA8B,gBAA4C;CACzI,QAAQ,KAAK,MAAb;EACI,KAAK,UAAU;GACX,MAAM,aAAa;GACnB,IAAI,WAAW,MAAM;IACjB,MAAM,YAAY,aAAa,UAAU;IACzC,MAAM,UAAU,kBAAkB,UAAU,IAAI;IAEhD,OAAO,IADQ,2BAA2B,UAAU,KAAK,WAAW,SAAS,WAAW,SAAS,SAC/E,KAAK,UAAU,GAAG,QAAQ;GAChD;GACA,IAAI,WAAW,SAAS,UAAU,WAAW,eAAe,QACxD,OAAO;GAEX,IAAI,WAAW,eAAe,UAAU,WAAW,IAAI,YAAY,WAAW,IAAI,WAC9E,OAAO;GAEX,IAAI,WAAW,eAAe,QAC1B,OAAO;GAEX,OAAO;EACX;EACA,KAAK,UAAU;GACX,MAAM,UAAU;GAChB,MAAM,OAAO,aAAa,UAAU,MAAM,UAAU;GACpD,IAAI,UAAU,WAAW,QAAQ,SAAS,aACtC,OAAO;GAEX,IAAI,QAAQ,YAAY;IACpB,IAAI,QAAQ,eAAe,oBAAoB,OAAO;IACtD,OAAO,QAAQ,WAAW,YAAY;GAC1C;GACA,OAAQ,QAAQ,YAAY,WAAW,OAAQ,YAAY;EAC/D;EACA,KAAK,WACD,OAAO;EACX,KAAK,QAAQ;GACT,MAAM,WAAW;GACjB,IAAI,SAAS,eAAe,QAAQ,OAAO;GAC3C,IAAI,SAAS,eAAe,QAAQ,OAAO;GAC3C,OAAO;EACX;EACA,KAAK,OAED,OAAO,KAAQ,eAAe,SAAS,SAAS;EAEpD,KAAK,SAAS;GACV,MAAM,YAAY;GAClB,IAAI,UAAU,UAAU;GACxB,IAAI,CAAC,WAAW,UAAU,MAAM,CAAC,MAAM,QAAQ,UAAU,EAAE,GAAG;IAC1D,MAAM,SAAS,UAAU;IACzB,IAAI,OAAO,SAAS,UAChB,UAAU;SACP,IAAI,OAAO,SAAS,UACvB,UAAU,OAAO,YAAY,UAAU,cAAc;SAClD,IAAI,OAAO,SAAS,WACvB,UAAU;GAElB;GACA,IAAI,YAAY,QAAQ,OAAO;GAC/B,IAAI,YAAY,UAAU,OAAO;GACjC,IAAI,YAAY,aAAa,OAAO;GACpC,IAAI,YAAY,aAAa,OAAO;GACpC,IAAI,YAAY,aAAa,OAAO;GACpC,OAAO;EACX;EACA,KAAK,UAED,OAAO,UAAU,KAAG,WAAW;EAEnC,KAAK,UACD,OAAO;EAEX,KAAK,YAAY;GACb,MAAM,UAAU;GAEhB,MAAM,WAAW,aADS,2BAA2B,UACvB,GAAmB,QAAQ,gBAAgB,QAAQ;GACjF,IAAI,CAAC,YAAY,SAAS,cAAc,YAAY,SAAS,gBAAgB,OACzE,MAAM,IAAI,MAAM,YAAY,SAAS,kDAAkD;GAE3F,IAAI;GACJ,IAAI;IACA,mBAAmB,SAAS,OAAO;GACvC,QAAQ;IACJ,OAAO;GACX;GACA,MAAM,SAAS,kBAAkB,gBAAgB;GACjD,OAAO,OAAO,SAAS,WAAW,YAAa,OAAO,SAAS,SAAS;EAC5E;EACA,KAAK,aAAa;GACd,MAAM,UAAU;GAChB,MAAM,mBAAmB,YAAY,MAAK,MAAK,EAAE,SAAS,QAAQ,QAAQ,aAAa,CAAC,MAAM,QAAQ,IAAI;GAC1G,IAAI,CAAC,kBAAkB,OAAO;GAC9B,MAAM,SAAS,kBAAkB,gBAAgB;GACjD,OAAO,OAAO,SAAS,WAAW,YAAa,OAAO,SAAS,SAAS;EAC5E;EACA,SACI,OAAO;CACf;AACJ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC7IA,IAAM,kBAAkB;AAExB,SAAS,qBAAqB,OAAe,MAAsB;CAC/D,IAAI,CAAC,gBAAgB,KAAK,KAAK,GAC3B,MAAM,IAAI,MAAM,wCAAwC,KAAK,IAAI,KAAK,UAAU,KAAK,GAAG;CAE5F,OAAO;AACX;AAuBA,SAAS,SAAS,YAAsC;CACpD,OAAO,2BAA2B,UAAU,KAAK,WAAW,SAAS,WAAW,SAAS;AAC7F;AAEA,SAAS,UAAU,YAAsC;CACrD,OAAO,GAAG,SAAS,UAAU,EAAE,GAAG,aAAa,UAAU;AAC7D;;;;;;;;;AAUA,SAAS,cAAc,YAAoE;CACvF,MAAM,QAAQ,aAAa,UAAU;CACrC,MAAM,SAAS,SAAS,UAAU;CAClC,MAAM,MAA4C,CAAC;CACnD,KAAK,MAAM,CAAC,UAAU,SAAS,OAAO,QAAQ,WAAW,cAAc,CAAC,CAAC,GAAG;EACxE,MAAM,IAAI;EACV,IAAI,EAAE,UAAU,MAAM,CAAC,EAAE,MAAM;EAC/B,IAAI,EAAE,SAAS,YAAY,EAAE,SAAS,UAAU;EAChD,MAAM,SAAU,EAAE,KACb,KAAI,UACD,SAAS,OAAO,UAAU,YAAY,QAAS,QACzC,OAAQ,MAAkC,EAAE,IAC5C,OAAO,KAAK,CACtB,EACC,QAAO,MAAK,EAAE,SAAS,CAAC;EAC7B,IAAI,OAAO,WAAW,GAAG;EACzB,IAAI,KAAK;GAAE,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,kBAAkB,UAAU,CAAC;GAAK;EAAO,CAAC;CACrF;CACA,OAAO;AACX;;AAGA,SAAS,aAAa,OAAuB;CACzC,OAAO,IAAI,MAAM,QAAQ,MAAM,IAAI,EAAE;AACzC;;;;;;;;AASA,SAAgB,2BACZ,aACA,UACU;CACV,MAAM,UAA0B,CAAC;CACjC,MAAM,+BAAe,IAAI,IAAY;CAIrC,KAAK,MAAM,cAAc,aACrB,KAAK,MAAM,EAAE,MAAM,YAAY,cAAc,UAAU,GAAG;EACtD,IAAI,SAAS,MAAM,IAAI,IAAI,KAAK,aAAa,IAAI,IAAI,GAAG;EACxD,aAAa,IAAI,IAAI;EACrB,MAAM,CAAC,QAAQ,YAAY,KAAK,MAAM,GAAG;EACzC,QAAQ,KAAK;GACT,MAAM;GACN,QAAQ;GACR,KAAK,gBAAgB,OAAO,KAAK,SAAS,aAAa,OAAO,IAAI,YAAY,EAAE,KAAK,IAAI,EAAE;EAC/F,CAAC;CACL;CAOJ,MAAM,0BAAU,IAAI,IAAY;CAChC,KAAK,MAAM,cAAc,aAAa;EAClC,MAAM,MAAM,UAAU,UAAU;EAChC,IAAI,SAAS,OAAO,IAAI,GAAG,KAAK,QAAQ,IAAI,GAAG,GAAG;EAClD,QAAQ,IAAI,GAAG;EACf,MAAM,SAAS,SAAS,UAAU;EAClC,MAAM,QAAQ,aAAa,UAAU;EACrC,MAAM,UAAU,OAAO,QAAQ,WAAW,cAAc,CAAC,CAAC,EAAE,MAAM,CAAC,GAAG,OAClE,aAAa,GAAG,GAAe,UAAU,CAC7C;EACA,MAAM,SAAS,UAAU,kBAAkB,QAAQ,IAAI,QAAQ,EAAc,IAAI;EACjF,MAAM,SAAS,UAAU;EACzB,IAAI;EACJ,IAAI,QAAQ,SAAS,UACjB,QAAQ,IAAI,OAAO;OAChB,IACH,UACA,OAAO,SAAS,YACf,OAA8B,SAAS,QAExC,QAAQ,IAAI,OAAO;OAEnB,QAAQ,IAAI,OAAO;EAEvB,QAAQ,KAAK;GACT,MAAM;GACN,QAAQ;GACR,KAAK,+BAA+B,OAAO,KAAK,MAAM,KAAK,MAAM;EACrE,CAAC;CACL;CAGA,KAAK,MAAM,cAAc,aAAa;EAClC,MAAM,MAAM,UAAU,UAAU;EAChC,MAAM,SAAS,SAAS,UAAU;EAClC,MAAM,QAAQ,aAAa,UAAU;EACrC,MAAM,UAAU,SAAS,OAAO,IAAI,GAAG,qBAAK,IAAI,IAAY;EAC5D,KAAK,MAAM,CAAC,UAAU,SAAS,OAAO,QAAQ,WAAW,cAAc,CAAC,CAAC,GAAG;GACxE,MAAM,IAAI;GACV,IAAI,aAAa,UAAU,GAAG,UAAU,GAAG;GAK3C,IAAI,EAAE,SAAS,eAAe,EAAE,SAAS,YAAY;GACrD,MAAM,SAAS,kBAAkB,UAAU,CAAC;GAC5C,IAAI,QAAQ,IAAI,MAAM,GAAG;GACzB,MAAM,OAAO,iBAAiB,UAAU,GAAG,YAAY,WAAW;GAClE,QAAQ,KAAK;IACT,MAAM;IACN,QAAQ,GAAG,IAAI,GAAG;IAIlB,KAAK,gBAAgB,OAAO,KAAK,MAAM,8BAA8B,OAAO,IAAI,KAAK;GACzF,CAAC;EACL;CACJ;CAEA,OAAO;EAAE;EAAS,YAAY,QAAQ,KAAI,MAAK,EAAE,GAAG;CAAE;AAC1D;;AAGA,eAAsB,mBAClB,QACA,SACuB;CACvB,MAAM,yBAAS,IAAI,IAAyB;CAC5C,MAAM,wBAAQ,IAAI,IAAY;CAC9B,IAAI,QAAQ,WAAW,GAAG,OAAO;EAAE;EAAQ;CAAM;CAEjD,MAAM,SAAS,QACV,KAAI,WAAU,IAAI,qBAAqB,QAAQ,aAAa,EAAE,EAAE,EAChE,KAAK,IAAI;CAEd,MAAM,EAAE,MAAM,YAAY,MAAM,OAAO,MAKnC;;kCAE0B,OAAO,EACrC;CACA,KAAK,MAAM,OAAO,SAAS;EACvB,MAAM,MAAM,GAAG,IAAI,aAAa,GAAG,IAAI;EACvC,IAAI,CAAC,OAAO,IAAI,GAAG,GAAG,OAAO,IAAI,qBAAK,IAAI,IAAI,CAAC;EAC/C,OAAO,IAAI,GAAG,EAAG,IAAI,IAAI,WAAW;CACxC;CAEA,MAAM,EAAE,MAAM,aAAa,MAAM,OAAO,MACpC;;;mDAG2C,OAAO,EACtD;CACA,KAAK,MAAM,OAAO,UAAU,MAAM,IAAI,GAAG,IAAI,OAAO,GAAG,IAAI,MAAM;CAEjE,OAAO;EAAE;EAAQ;CAAM;AAC3B;;;;;;;;;AAUA,eAAsB,uBAClB,QACA,aACA,KACmB;CACnB,MAAM,UAAU,MAAM,KAAK,IAAI,IAAI,YAAY,IAAI,QAAQ,CAAC,CAAC;CAC7D,KAAK,MAAM,UAAU,SAAS;EAC1B,qBAAqB,QAAQ,aAAa;EAC1C,IAAI,WAAW,UACX,MAAM,OAAO,MAAM,gCAAgC,OAAO,GAAG;CAErE;CAGA,MAAM,OAAO,2BAA2B,aAAa,MAD9B,mBAAmB,QAAQ,OAAO,CACI;CAE7D,IAAI,KAAK,QAAQ,WAAW,GAAG;EAC3B,MAAM,0CAA0C;EAChD,OAAO;CACX;CAEA,KAAK,MAAM,UAAU,KAAK,SACtB,IAAI;EACA,MAAM,OAAO,MAAM,OAAO,GAAG;EAC7B,MAAM,GAAG,OAAO,KAAK,IAAI,OAAO,QAAQ;CAC5C,SAAS,KAAK;EACV,MAAM,IAAI,MACN,aAAa,OAAO,KAAK,GAAG,OAAO,OAAO,IACvC,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EAAE,MAAM,OAAO,KACrE;CACJ;CAEJ,OAAO;AACX"}
|