@rebasepro/server-postgresql 0.0.1-canary.4d4fb3e
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 +6 -0
- package/README.md +106 -0
- package/build-errors.txt +37 -0
- package/dist/common/src/collections/CollectionRegistry.d.ts +48 -0
- package/dist/common/src/collections/index.d.ts +1 -0
- package/dist/common/src/data/buildRebaseData.d.ts +14 -0
- package/dist/common/src/index.d.ts +3 -0
- package/dist/common/src/util/builders.d.ts +57 -0
- package/dist/common/src/util/callbacks.d.ts +6 -0
- package/dist/common/src/util/collections.d.ts +11 -0
- package/dist/common/src/util/common.d.ts +2 -0
- package/dist/common/src/util/conditions.d.ts +26 -0
- package/dist/common/src/util/entities.d.ts +36 -0
- package/dist/common/src/util/enums.d.ts +3 -0
- package/dist/common/src/util/index.d.ts +16 -0
- package/dist/common/src/util/navigation_from_path.d.ts +34 -0
- package/dist/common/src/util/navigation_utils.d.ts +20 -0
- package/dist/common/src/util/parent_references_from_path.d.ts +6 -0
- package/dist/common/src/util/paths.d.ts +14 -0
- package/dist/common/src/util/permissions.d.ts +5 -0
- package/dist/common/src/util/references.d.ts +2 -0
- package/dist/common/src/util/relations.d.ts +12 -0
- package/dist/common/src/util/resolutions.d.ts +72 -0
- package/dist/common/src/util/storage.d.ts +24 -0
- package/dist/index.es.js +10635 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.js +10643 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +112 -0
- package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +40 -0
- package/dist/server-postgresql/src/auth/ensure-tables.d.ts +6 -0
- package/dist/server-postgresql/src/auth/services.d.ts +188 -0
- package/dist/server-postgresql/src/cli.d.ts +1 -0
- package/dist/server-postgresql/src/collections/PostgresCollectionRegistry.d.ts +43 -0
- package/dist/server-postgresql/src/connection.d.ts +7 -0
- package/dist/server-postgresql/src/data-transformer.d.ts +36 -0
- package/dist/server-postgresql/src/databasePoolManager.d.ts +20 -0
- package/dist/server-postgresql/src/history/HistoryService.d.ts +71 -0
- package/dist/server-postgresql/src/history/ensure-history-table.d.ts +7 -0
- package/dist/server-postgresql/src/index.d.ts +13 -0
- package/dist/server-postgresql/src/interfaces.d.ts +18 -0
- package/dist/server-postgresql/src/schema/auth-schema.d.ts +767 -0
- package/dist/server-postgresql/src/schema/generate-drizzle-schema-logic.d.ts +2 -0
- package/dist/server-postgresql/src/schema/generate-drizzle-schema.d.ts +1 -0
- package/dist/server-postgresql/src/services/BranchService.d.ts +47 -0
- package/dist/server-postgresql/src/services/EntityFetchService.d.ts +195 -0
- package/dist/server-postgresql/src/services/EntityPersistService.d.ts +41 -0
- package/dist/server-postgresql/src/services/RelationService.d.ts +92 -0
- package/dist/server-postgresql/src/services/entity-helpers.d.ts +24 -0
- package/dist/server-postgresql/src/services/entityService.d.ts +102 -0
- package/dist/server-postgresql/src/services/index.d.ts +4 -0
- package/dist/server-postgresql/src/services/realtimeService.d.ts +186 -0
- package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +116 -0
- package/dist/server-postgresql/src/websocket.d.ts +5 -0
- package/dist/types/src/controllers/analytics_controller.d.ts +7 -0
- package/dist/types/src/controllers/auth.d.ts +117 -0
- package/dist/types/src/controllers/client.d.ts +58 -0
- package/dist/types/src/controllers/collection_registry.d.ts +44 -0
- package/dist/types/src/controllers/customization_controller.d.ts +54 -0
- package/dist/types/src/controllers/data.d.ts +141 -0
- package/dist/types/src/controllers/data_driver.d.ts +168 -0
- package/dist/types/src/controllers/database_admin.d.ts +11 -0
- package/dist/types/src/controllers/dialogs_controller.d.ts +36 -0
- package/dist/types/src/controllers/effective_role.d.ts +4 -0
- package/dist/types/src/controllers/index.d.ts +17 -0
- package/dist/types/src/controllers/local_config_persistence.d.ts +20 -0
- package/dist/types/src/controllers/navigation.d.ts +213 -0
- package/dist/types/src/controllers/registry.d.ts +51 -0
- package/dist/types/src/controllers/side_dialogs_controller.d.ts +67 -0
- package/dist/types/src/controllers/side_entity_controller.d.ts +89 -0
- package/dist/types/src/controllers/snackbar.d.ts +24 -0
- package/dist/types/src/controllers/storage.d.ts +173 -0
- package/dist/types/src/index.d.ts +4 -0
- package/dist/types/src/rebase_context.d.ts +101 -0
- package/dist/types/src/types/backend.d.ts +533 -0
- package/dist/types/src/types/builders.d.ts +14 -0
- package/dist/types/src/types/chips.d.ts +5 -0
- package/dist/types/src/types/collections.d.ts +812 -0
- package/dist/types/src/types/data_source.d.ts +64 -0
- package/dist/types/src/types/entities.d.ts +145 -0
- package/dist/types/src/types/entity_actions.d.ts +98 -0
- package/dist/types/src/types/entity_callbacks.d.ts +173 -0
- package/dist/types/src/types/entity_link_builder.d.ts +7 -0
- package/dist/types/src/types/entity_overrides.d.ts +9 -0
- package/dist/types/src/types/entity_views.d.ts +61 -0
- package/dist/types/src/types/export_import.d.ts +21 -0
- package/dist/types/src/types/index.d.ts +22 -0
- package/dist/types/src/types/locales.d.ts +4 -0
- package/dist/types/src/types/modify_collections.d.ts +5 -0
- package/dist/types/src/types/plugins.d.ts +225 -0
- package/dist/types/src/types/properties.d.ts +1091 -0
- package/dist/types/src/types/property_config.d.ts +70 -0
- package/dist/types/src/types/relations.d.ts +336 -0
- package/dist/types/src/types/slots.d.ts +228 -0
- package/dist/types/src/types/translations.d.ts +826 -0
- package/dist/types/src/types/user_management_delegate.d.ts +120 -0
- package/dist/types/src/types/websockets.d.ts +78 -0
- package/dist/types/src/users/index.d.ts +2 -0
- package/dist/types/src/users/roles.d.ts +22 -0
- package/dist/types/src/users/user.d.ts +46 -0
- package/jest-all.log +3128 -0
- package/jest.log +49 -0
- package/package.json +93 -0
- package/src/PostgresBackendDriver.ts +1024 -0
- package/src/PostgresBootstrapper.ts +232 -0
- package/src/auth/ensure-tables.ts +309 -0
- package/src/auth/services.ts +740 -0
- package/src/cli.ts +347 -0
- package/src/collections/PostgresCollectionRegistry.ts +96 -0
- package/src/connection.ts +62 -0
- package/src/data-transformer.ts +569 -0
- package/src/databasePoolManager.ts +84 -0
- package/src/history/HistoryService.ts +257 -0
- package/src/history/ensure-history-table.ts +45 -0
- package/src/index.ts +13 -0
- package/src/interfaces.ts +60 -0
- package/src/schema/auth-schema.ts +146 -0
- package/src/schema/generate-drizzle-schema-logic.ts +618 -0
- package/src/schema/generate-drizzle-schema.ts +151 -0
- package/src/services/BranchService.ts +237 -0
- package/src/services/EntityFetchService.ts +1447 -0
- package/src/services/EntityPersistService.ts +351 -0
- package/src/services/RelationService.ts +1012 -0
- package/src/services/entity-helpers.ts +121 -0
- package/src/services/entityService.ts +209 -0
- package/src/services/index.ts +13 -0
- package/src/services/realtimeService.ts +1005 -0
- package/src/utils/drizzle-conditions.ts +999 -0
- package/src/websocket.ts +487 -0
- package/test/auth-services.test.ts +569 -0
- package/test/branchService.test.ts +357 -0
- package/test/drizzle-conditions.test.ts +895 -0
- package/test/entityService.errors.test.ts +352 -0
- package/test/entityService.relations.test.ts +912 -0
- package/test/entityService.subcollection-search.test.ts +516 -0
- package/test/entityService.test.ts +977 -0
- package/test/generate-drizzle-schema.test.ts +795 -0
- package/test/historyService.test.ts +126 -0
- package/test/postgresDataDriver.test.ts +556 -0
- package/test/realtimeService.test.ts +276 -0
- package/test/relations.test.ts +662 -0
- package/test_drizzle_mock.js +3 -0
- package/test_find_changed.mjs +30 -0
- package/test_output.txt +3145 -0
- package/tsconfig.json +49 -0
- package/tsconfig.prod.json +20 -0
- package/vite.config.ts +82 -0
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
import { EntityCollection, NumberProperty, Property, Relation, RelationProperty, SecurityOperation, SecurityRule, StringProperty } from "@rebasepro/types";
|
|
2
|
+
import { getPrimaryKeys } from "../services/entity-helpers";
|
|
3
|
+
import { getEnumVarName, getTableName, getTableVarName, resolveCollectionRelations } from "@rebasepro/common";
|
|
4
|
+
import { toSnakeCase } from "@rebasepro/utils";
|
|
5
|
+
|
|
6
|
+
// --- Helper Functions ---
|
|
7
|
+
|
|
8
|
+
const getPrimaryKeyProp = (collection: EntityCollection): { name: string, type: "string" | "number" } => {
|
|
9
|
+
if (collection.properties) {
|
|
10
|
+
const idPropEntry = Object.entries(collection.properties).find(([_, prop]) => "isId" in (prop as object) && Boolean((prop as unknown as Record<string, unknown>).isId));
|
|
11
|
+
if (idPropEntry) {
|
|
12
|
+
return { name: idPropEntry[0], type: (idPropEntry[1] as Property).type === "number" ? "number" : "string" };
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
// Fallback
|
|
16
|
+
const idProp = collection.properties?.["id"] as Property | undefined;
|
|
17
|
+
if (idProp?.type === "number") {
|
|
18
|
+
return { name: "id", type: "number" };
|
|
19
|
+
}
|
|
20
|
+
return { name: "id", type: "string" };
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const isNumericId = (collection: EntityCollection): boolean => {
|
|
24
|
+
return getPrimaryKeyProp(collection).type === "number";
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const getPrimaryKeyName = (collection: EntityCollection): string => {
|
|
28
|
+
return getPrimaryKeyProp(collection).name;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const isIdProperty = (propName: string, prop: Property, collection: EntityCollection): boolean => {
|
|
32
|
+
if ("isId" in prop && Boolean(prop.isId)) return true;
|
|
33
|
+
|
|
34
|
+
// We only fallback to "id" if NO property is explicitly marked with `isId: true` or a generator string
|
|
35
|
+
const hasExplicitId = Object.values(collection.properties ?? {}).some(p => "isId" in (p as object) && Boolean((p as unknown as Record<string, unknown>).isId));
|
|
36
|
+
return !hasExplicitId && propName === "id";
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const getDrizzleColumn = (propName: string, prop: Property, collection: EntityCollection): string | null => {
|
|
40
|
+
const colName = toSnakeCase(propName);
|
|
41
|
+
let columnDefinition: string;
|
|
42
|
+
|
|
43
|
+
switch (prop.type) {
|
|
44
|
+
case "string": {
|
|
45
|
+
const stringProp = prop as StringProperty;
|
|
46
|
+
if (stringProp.enum) {
|
|
47
|
+
const enumName = getEnumVarName(getTableName(collection), propName);
|
|
48
|
+
columnDefinition = `${enumName}("${colName}")`;
|
|
49
|
+
} else if ("isId" in stringProp && stringProp.isId === "uuid") {
|
|
50
|
+
columnDefinition = `uuid("${colName}")`;
|
|
51
|
+
} else if (stringProp.columnType === "text") {
|
|
52
|
+
columnDefinition = `text("${colName}")`;
|
|
53
|
+
} else if (stringProp.columnType === "char") {
|
|
54
|
+
columnDefinition = `char("${colName}")`;
|
|
55
|
+
} else {
|
|
56
|
+
columnDefinition = `varchar("${colName}")`;
|
|
57
|
+
}
|
|
58
|
+
if (isIdProperty(propName, prop, collection)) {
|
|
59
|
+
columnDefinition += `.primaryKey()`;
|
|
60
|
+
}
|
|
61
|
+
if ("isId" in stringProp && stringProp.isId !== "manual" && stringProp.isId !== true) {
|
|
62
|
+
if (stringProp.isId === "uuid") {
|
|
63
|
+
columnDefinition += `.defaultRandom()`;
|
|
64
|
+
} else if (stringProp.isId === "cuid") {
|
|
65
|
+
columnDefinition += `.default(sql\`cuid()\`)`;
|
|
66
|
+
} else if (typeof stringProp.isId === "string") {
|
|
67
|
+
const sqlContent = stringProp.isId.startsWith("sql`") && stringProp.isId.endsWith("`")
|
|
68
|
+
? stringProp.isId.substring(4, stringProp.isId.length - 1)
|
|
69
|
+
: stringProp.isId;
|
|
70
|
+
columnDefinition += `.default(sql\`${sqlContent}\`)`;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (stringProp.validation?.unique) {
|
|
74
|
+
columnDefinition += `.unique()`;
|
|
75
|
+
}
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
case "number": {
|
|
79
|
+
const numProp = prop as NumberProperty;
|
|
80
|
+
const isId = isIdProperty(propName, prop, collection);
|
|
81
|
+
|
|
82
|
+
let baseType = (numProp.validation?.integer || isId) ? `integer("${colName}")` : `numeric("${colName}")`;
|
|
83
|
+
if (numProp.columnType) {
|
|
84
|
+
if (numProp.columnType === "double precision") baseType = `doublePrecision("${colName}")`;
|
|
85
|
+
else baseType = `${numProp.columnType}("${colName}")`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if ("isId" in numProp && numProp.isId === "increment") {
|
|
89
|
+
columnDefinition = `${baseType}.generatedByDefaultAsIdentity()`;
|
|
90
|
+
} else if ("isId" in numProp && typeof numProp.isId === "string" && numProp.isId !== "manual") {
|
|
91
|
+
columnDefinition = baseType;
|
|
92
|
+
const sqlContent = numProp.isId.startsWith("sql`") && numProp.isId.endsWith("`")
|
|
93
|
+
? numProp.isId.substring(4, numProp.isId.length - 1)
|
|
94
|
+
: numProp.isId;
|
|
95
|
+
columnDefinition += `.default(sql\`${sqlContent}\`)`;
|
|
96
|
+
} else {
|
|
97
|
+
columnDefinition = baseType;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (isId) {
|
|
101
|
+
columnDefinition += `.primaryKey()`;
|
|
102
|
+
}
|
|
103
|
+
if (numProp.validation?.unique) {
|
|
104
|
+
columnDefinition += `.unique()`;
|
|
105
|
+
}
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
case "boolean":
|
|
109
|
+
columnDefinition = `boolean("${colName}")`;
|
|
110
|
+
break;
|
|
111
|
+
case "date": {
|
|
112
|
+
const dateProp = prop as import("@rebasepro/types").DateProperty;
|
|
113
|
+
if (dateProp.columnType === "date") {
|
|
114
|
+
columnDefinition = `date("${colName}", { mode: 'string' })`;
|
|
115
|
+
} else if (dateProp.columnType === "time") {
|
|
116
|
+
columnDefinition = `time("${colName}")`;
|
|
117
|
+
} else {
|
|
118
|
+
columnDefinition = `timestamp("${colName}", { withTimezone: true, mode: 'string' })`;
|
|
119
|
+
}
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
case "map":
|
|
123
|
+
case "array": {
|
|
124
|
+
const arrayOrMapProp = prop as import("@rebasepro/types").ArrayProperty | import("@rebasepro/types").MapProperty;
|
|
125
|
+
if (arrayOrMapProp.columnType === "json") {
|
|
126
|
+
columnDefinition = `json("${colName}")`;
|
|
127
|
+
} else {
|
|
128
|
+
columnDefinition = `jsonb("${colName}")`;
|
|
129
|
+
}
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
case "relation": {
|
|
133
|
+
const refProp = prop as RelationProperty;
|
|
134
|
+
const resolvedRelations = resolveCollectionRelations(collection as import("@rebasepro/types").PostgresCollection<any, any>);
|
|
135
|
+
const relation = resolvedRelations[refProp.relationName ?? propName];
|
|
136
|
+
|
|
137
|
+
// Only owning one-to-one/many-to-one relations create a column here.
|
|
138
|
+
if (!relation || relation.direction !== "owning" || relation.cardinality !== "one") {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// The localKey property is the source of truth for the FK column name.
|
|
143
|
+
if (!relation.localKey) {
|
|
144
|
+
console.warn(`Could not generate column for owning relation '${relation.relationName}' on '${collection.name}': 'localKey' is not defined.`);
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// If the localKey property is defined elsewhere in the properties, it will be handled there.
|
|
149
|
+
// This logic is for when the relation property itself defines the FK.
|
|
150
|
+
if (collection.properties[relation.localKey] && propName !== relation.localKey) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let targetCollection: EntityCollection;
|
|
155
|
+
try {
|
|
156
|
+
targetCollection = relation.target();
|
|
157
|
+
} catch {
|
|
158
|
+
return null; // Cannot resolve target
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const fkColumnName = toSnakeCase(relation.localKey);
|
|
162
|
+
const targetTableVar = getTableVarName(getTableName(targetCollection));
|
|
163
|
+
const targetIdField = getPrimaryKeyName(targetCollection);
|
|
164
|
+
const baseColumn = isNumericId(targetCollection) ? `integer(\"${fkColumnName}\")` : `varchar(\"${fkColumnName}\")`;
|
|
165
|
+
|
|
166
|
+
const onUpdate = relation.onUpdate ? `onUpdate: \"${relation.onUpdate}\"` : "";
|
|
167
|
+
const required = prop.validation?.required;
|
|
168
|
+
const onDeleteVal = relation.onDelete ?? (required ? "cascade" : "set null");
|
|
169
|
+
const onDelete = `onDelete: \"${onDeleteVal}\"`;
|
|
170
|
+
|
|
171
|
+
const refOptionsParts = [onUpdate, onDelete].filter(Boolean);
|
|
172
|
+
const refOptions = refOptionsParts.length > 0 ? `{ ${refOptionsParts.join(", ")} }` : "";
|
|
173
|
+
|
|
174
|
+
let columnDef = `${baseColumn}.references(() => ${targetTableVar}.${targetIdField}${refOptions ? `, ${refOptions}` : ""})`;
|
|
175
|
+
|
|
176
|
+
if (required) {
|
|
177
|
+
columnDef += ".notNull()";
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return ` ${relation.localKey}: ${columnDef}`;
|
|
181
|
+
}
|
|
182
|
+
default:
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (prop.validation?.required) {
|
|
187
|
+
columnDefinition += ".notNull()";
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return ` ${propName}: ${columnDefinition}`;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Resolves a raw SQL string, replacing `{column_name}` with `${table.column_name}`.
|
|
195
|
+
* The result is wrapped in a Drizzle sql`` template literal.
|
|
196
|
+
*/
|
|
197
|
+
const resolveRawSql = (expression: string): string => {
|
|
198
|
+
// Replace {column_name} with ${table.column_name}
|
|
199
|
+
const resolved = expression.replace(/\{(\w+)\}/g, (_, col) => `\${table.${col}}`);
|
|
200
|
+
return `sql\`${resolved}\``;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Wraps a SQL clause with a role check using AND.
|
|
205
|
+
* Generates: `(<clause>) AND (string_to_array(auth.roles(), ',') && ARRAY['<role1>','<role2>'])`
|
|
206
|
+
*/
|
|
207
|
+
const wrapWithRoleCheck = (clause: string, roles: string[]): string => {
|
|
208
|
+
const rolesArrayString = `ARRAY[${roles.map(r => `'${r}'`).join(',')}]`;
|
|
209
|
+
const roleCondition = `string_to_array(auth.roles(), ',') @> ${rolesArrayString}`;
|
|
210
|
+
return `sql\`(${unwrapSql(clause)}) AND (${roleCondition})\``;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Extracts the inner expression from a `sql\`...\`` wrapper.
|
|
215
|
+
*/
|
|
216
|
+
const unwrapSql = (sqlExpr: string): string => {
|
|
217
|
+
const match = sqlExpr.match(/^sql`(.*)`$/s);
|
|
218
|
+
return match ? match[1] : sqlExpr;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Builds the USING clause for a policy based on shortcuts or raw SQL.
|
|
223
|
+
*/
|
|
224
|
+
const buildUsingClause = (rule: SecurityRule): string | null => {
|
|
225
|
+
if (rule.using) {
|
|
226
|
+
return resolveRawSql(rule.using);
|
|
227
|
+
}
|
|
228
|
+
if (rule.access === "public") {
|
|
229
|
+
return `sql\`true\``;
|
|
230
|
+
}
|
|
231
|
+
if (rule.ownerField) {
|
|
232
|
+
return `sql\`\${table.${rule.ownerField}} = auth.uid()\``;
|
|
233
|
+
}
|
|
234
|
+
return null;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Builds the WITH CHECK clause for a policy based on shortcuts or raw SQL.
|
|
239
|
+
* Falls back to the USING clause if not explicitly provided.
|
|
240
|
+
*/
|
|
241
|
+
const buildWithCheckClause = (rule: SecurityRule): string | null => {
|
|
242
|
+
if (rule.withCheck) {
|
|
243
|
+
return resolveRawSql(rule.withCheck);
|
|
244
|
+
}
|
|
245
|
+
// For insert/update/all, fall back to using clause if withCheck not specified
|
|
246
|
+
return buildUsingClause(rule);
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Generates Drizzle pgPolicy() calls from a declarative SecurityRule definition.
|
|
251
|
+
*
|
|
252
|
+
* Supports the full spectrum:
|
|
253
|
+
* - Convenience shortcuts: ownerField, access, roles
|
|
254
|
+
* - Raw SQL: using, withCheck
|
|
255
|
+
* - Mode: permissive (default) or restrictive
|
|
256
|
+
* - operations[] array: generates one policy per operation
|
|
257
|
+
* - Combinations: roles + ownerField, roles + raw SQL, etc.
|
|
258
|
+
*/
|
|
259
|
+
const generatePolicyCode = (tableName: string, rule: SecurityRule, index: number): string => {
|
|
260
|
+
// Resolve operations: operations[] takes precedence over operation (singular)
|
|
261
|
+
const ops: SecurityOperation[] = rule.operations && rule.operations.length > 0
|
|
262
|
+
? rule.operations
|
|
263
|
+
: [rule.operation ?? "all"];
|
|
264
|
+
|
|
265
|
+
// Generate one pgPolicy per operation
|
|
266
|
+
return ops.map((op, opIdx) => {
|
|
267
|
+
const policyName = rule.name
|
|
268
|
+
? (ops.length > 1 ? `${rule.name}_${op}` : rule.name)
|
|
269
|
+
: `${tableName}_${op}_policy_${index}${ops.length > 1 ? `_${opIdx}` : ""}`;
|
|
270
|
+
|
|
271
|
+
return generateSinglePolicyCode(tableName, rule, op, policyName);
|
|
272
|
+
}).join("");
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Generates a single pgPolicy() call for one specific operation.
|
|
277
|
+
*/
|
|
278
|
+
const generateSinglePolicyCode = (tableName: string, rule: SecurityRule, operation: SecurityOperation, policyName: string): string => {
|
|
279
|
+
const mode = rule.mode ?? "permissive";
|
|
280
|
+
const roles = rule.roles;
|
|
281
|
+
|
|
282
|
+
// Determine which clauses this operation needs:
|
|
283
|
+
// SELECT, DELETE → USING only
|
|
284
|
+
// INSERT → WITH CHECK only
|
|
285
|
+
// UPDATE, ALL → both USING and WITH CHECK
|
|
286
|
+
const needsUsing = operation !== "insert";
|
|
287
|
+
const needsWithCheck = operation !== "select" && operation !== "delete";
|
|
288
|
+
|
|
289
|
+
let usingClause = needsUsing ? buildUsingClause(rule) : null;
|
|
290
|
+
let withCheckClause = needsWithCheck ? buildWithCheckClause(rule) : null;
|
|
291
|
+
|
|
292
|
+
// If roles are specified, wrap existing clauses with role check,
|
|
293
|
+
// or generate a roles-only clause.
|
|
294
|
+
if (roles && roles.length > 0) {
|
|
295
|
+
if (usingClause) {
|
|
296
|
+
usingClause = wrapWithRoleCheck(usingClause, roles);
|
|
297
|
+
} else if (needsUsing) {
|
|
298
|
+
// Roles-only rule (e.g. { operation: "select", roles: ["admin"] })
|
|
299
|
+
const rolesArrayString = `ARRAY[${roles.map(r => `'${r}'`).join(',')}]`;
|
|
300
|
+
usingClause = `sql\`string_to_array(auth.roles(), ',') @> ${rolesArrayString}\``;
|
|
301
|
+
}
|
|
302
|
+
if (withCheckClause) {
|
|
303
|
+
withCheckClause = wrapWithRoleCheck(withCheckClause, roles);
|
|
304
|
+
} else if (needsWithCheck) {
|
|
305
|
+
const rolesArrayString = `ARRAY[${roles.map(r => `'${r}'`).join(',')}]`;
|
|
306
|
+
withCheckClause = `sql\`string_to_array(auth.roles(), ',') @> ${rolesArrayString}\``;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Fallback: if we still have no clauses, deny all (safety net)
|
|
311
|
+
if (!usingClause && needsUsing) {
|
|
312
|
+
usingClause = `sql\`false\``;
|
|
313
|
+
}
|
|
314
|
+
if (!withCheckClause && needsWithCheck) {
|
|
315
|
+
withCheckClause = `sql\`false\``;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Build the policy options object
|
|
319
|
+
const parts: string[] = [];
|
|
320
|
+
parts.push(`as: "${mode}"`);
|
|
321
|
+
parts.push(`for: "${operation}"`);
|
|
322
|
+
const toRoles = rule.pgRoles ?? ["public"];
|
|
323
|
+
parts.push(`to: [${toRoles.map(r => `"${r}"`).join(", ")}]`);
|
|
324
|
+
if (usingClause) parts.push(`using: ${usingClause}`);
|
|
325
|
+
if (withCheckClause) parts.push(`withCheck: ${withCheckClause}`);
|
|
326
|
+
|
|
327
|
+
return ` pgPolicy("${policyName}", { ${parts.join(", ")} }),\n`;
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
// --- Main Schema Generation Logic ---
|
|
331
|
+
export const generateSchema = async (collections: EntityCollection[]): Promise<string> => {
|
|
332
|
+
let schemaContent = "// This file is auto-generated by the Rebase Drizzle generator. Do not edit manually.\n\n";
|
|
333
|
+
|
|
334
|
+
const hasUuid = collections.some(c =>
|
|
335
|
+
c.properties && Object.values(c.properties).some(
|
|
336
|
+
(p: Property) => p.type === "string" && ((p as unknown as Record<string, unknown>).autoValue === "uuid" || (p as unknown as Record<string, unknown>).isId === "uuid")
|
|
337
|
+
)
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
const hasJson = collections.some(c =>
|
|
341
|
+
c.properties && Object.values(c.properties).some(
|
|
342
|
+
(p: Property) => (p.type === "map" || p.type === "array") && (p as unknown as Record<string, unknown>).columnType === "json"
|
|
343
|
+
)
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
// Always import pgPolicy and sql — RLS is enabled on every table (secure by default)
|
|
347
|
+
const pgCoreImports = ["primaryKey", "pgTable", "integer", "varchar", "text", "char", "boolean", "timestamp", "date", "time", "jsonb", "json", "pgEnum", "numeric", "real", "doublePrecision", "bigint", "serial", "bigserial", "pgPolicy"];
|
|
348
|
+
if (hasUuid) pgCoreImports.push("uuid");
|
|
349
|
+
schemaContent += `import { ${pgCoreImports.join(", ")} } from 'drizzle-orm/pg-core';\n`;
|
|
350
|
+
schemaContent += `import { relations as drizzleRelations, sql } from 'drizzle-orm';\n\n`;
|
|
351
|
+
|
|
352
|
+
const exportedTableVars: string[] = [];
|
|
353
|
+
const exportedEnumVars: string[] = [];
|
|
354
|
+
const exportedRelationVars: string[] = [];
|
|
355
|
+
|
|
356
|
+
const allTablesToGenerate = new Map<string, {
|
|
357
|
+
collection: EntityCollection,
|
|
358
|
+
isJunction?: boolean,
|
|
359
|
+
relation?: Relation,
|
|
360
|
+
sourceCollection?: EntityCollection
|
|
361
|
+
}>();
|
|
362
|
+
|
|
363
|
+
// 1. Generate Enums
|
|
364
|
+
collections.forEach(collection => {
|
|
365
|
+
const collectionPath = getTableName(collection);
|
|
366
|
+
Object.entries(collection.properties ?? {}).forEach(([propName, prop]) => {
|
|
367
|
+
if (("enum" in prop) && (prop.type === "string" || prop.type === "number") && prop.enum) {
|
|
368
|
+
const enumVarName = getEnumVarName(collectionPath, propName);
|
|
369
|
+
const enumDbName = `${collectionPath}_${toSnakeCase(propName)}`;
|
|
370
|
+
const values = Array.isArray(prop.enum)
|
|
371
|
+
? prop.enum.map(v => String(v.id ?? v))
|
|
372
|
+
: Object.keys(prop.enum);
|
|
373
|
+
if (values.length > 0) {
|
|
374
|
+
schemaContent += `export const ${enumVarName} = pgEnum(\"${enumDbName}\", [${values.map(v => `'${v}'`).join(", ")}]);\n`;
|
|
375
|
+
if (!exportedEnumVars.includes(enumVarName)) exportedEnumVars.push(enumVarName);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
schemaContent += "\n";
|
|
381
|
+
|
|
382
|
+
// 2. Identify all tables (collections and junction tables only)
|
|
383
|
+
for (const collection of collections) {
|
|
384
|
+
const tableName = getTableName(collection);
|
|
385
|
+
if (tableName) {
|
|
386
|
+
allTablesToGenerate.set(tableName, { collection });
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const resolvedRelations = resolveCollectionRelations(collection as import("@rebasepro/types").PostgresCollection<any, any>);
|
|
390
|
+
for (const relation of Object.values(resolvedRelations)) {
|
|
391
|
+
if (relation.through) { // Standard M2M junction table
|
|
392
|
+
const junctionTableName = relation.through.table;
|
|
393
|
+
if (!allTablesToGenerate.has(junctionTableName)) {
|
|
394
|
+
allTablesToGenerate.set(junctionTableName, {
|
|
395
|
+
collection: {
|
|
396
|
+
table: junctionTableName,
|
|
397
|
+
properties: {}
|
|
398
|
+
} as EntityCollection,
|
|
399
|
+
isJunction: true,
|
|
400
|
+
relation: relation,
|
|
401
|
+
sourceCollection: collection
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
// joinPath relations use existing user-controlled tables - no generation needed
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// 3. Generate pgTable definitions for all unique tables
|
|
410
|
+
for (const [tableName, {
|
|
411
|
+
collection,
|
|
412
|
+
isJunction,
|
|
413
|
+
relation,
|
|
414
|
+
sourceCollection
|
|
415
|
+
}] of allTablesToGenerate.entries()) {
|
|
416
|
+
const tableVarName = getTableVarName(tableName);
|
|
417
|
+
if (isJunction && relation && sourceCollection && relation.through) {
|
|
418
|
+
const targetCollection = relation.target();
|
|
419
|
+
const {
|
|
420
|
+
sourceColumn,
|
|
421
|
+
targetColumn
|
|
422
|
+
} = relation.through;
|
|
423
|
+
|
|
424
|
+
const onDelete = relation.onDelete ?? "cascade";
|
|
425
|
+
const refOptions = `{ onDelete: \"${onDelete}\" }`;
|
|
426
|
+
|
|
427
|
+
const sourceColType = isNumericId(sourceCollection) ? "integer" : "varchar";
|
|
428
|
+
const targetColType = isNumericId(targetCollection) ? "integer" : "varchar";
|
|
429
|
+
const sourceId = getPrimaryKeyName(sourceCollection);
|
|
430
|
+
const targetId = getPrimaryKeyName(targetCollection);
|
|
431
|
+
|
|
432
|
+
schemaContent += `export const ${tableVarName} = pgTable(\"${tableName}\", {\n`;
|
|
433
|
+
schemaContent += ` ${sourceColumn}: ${sourceColType}(\"${toSnakeCase(sourceColumn)}\").notNull().references(() => ${getTableVarName(getTableName(sourceCollection))}.${sourceId}, ${refOptions}),\n`;
|
|
434
|
+
schemaContent += ` ${targetColumn}: ${targetColType}(\"${toSnakeCase(targetColumn)}\").notNull().references(() => ${getTableVarName(getTableName(targetCollection))}.${targetId}, ${refOptions}),\n`;
|
|
435
|
+
schemaContent += `}, (table) => ({\n`;
|
|
436
|
+
schemaContent += ` pk: primaryKey({ columns: [table.${sourceColumn}, table.${targetColumn}] })\n`;
|
|
437
|
+
schemaContent += `}));\n\n`;
|
|
438
|
+
} else if (!isJunction) {
|
|
439
|
+
schemaContent += `export const ${tableVarName} = pgTable(\"${tableName}\", {\n`;
|
|
440
|
+
const columns = new Set<string>();
|
|
441
|
+
Object.entries(collection.properties ?? {}).forEach(([propName, prop]) => {
|
|
442
|
+
const columnString = getDrizzleColumn(propName, prop as Property, collection);
|
|
443
|
+
if (columnString) columns.add(columnString);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
// Backwards compatibility: if no id/primary key column is found in properties, but `id` wasn't explicitly provided
|
|
447
|
+
// We should generate a basic id column if one was completely omitted.
|
|
448
|
+
const hasIdColumn = Array.from(columns).some(col => col.includes(".primaryKey()"));
|
|
449
|
+
if (!hasIdColumn) {
|
|
450
|
+
columns.add(` id: varchar(\"id\").primaryKey()`);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
schemaContent += `${Array.from(columns).join(",\n")}`;
|
|
454
|
+
|
|
455
|
+
const securityRules = (collection as import("@rebasepro/types").PostgresCollection<Record<string, unknown>, import("@rebasepro/types").User>).securityRules;
|
|
456
|
+
if (securityRules && securityRules.length > 0) {
|
|
457
|
+
schemaContent += "\n}, (table) => ([\n";
|
|
458
|
+
securityRules.forEach((rule: SecurityRule, idx: number) => {
|
|
459
|
+
schemaContent += generatePolicyCode(tableName, rule, idx);
|
|
460
|
+
});
|
|
461
|
+
schemaContent += "])).enableRLS();\n\n";
|
|
462
|
+
} else {
|
|
463
|
+
// No explicit policies — RLS enabled with deny-all default (Postgres denies
|
|
464
|
+
// everything when RLS is on and no permissive policies exist).
|
|
465
|
+
schemaContent += "\n}).enableRLS();\n\n";
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
if (!exportedTableVars.includes(tableVarName)) exportedTableVars.push(tableVarName);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// 4. Generate Drizzle Relations
|
|
472
|
+
for (const [tableName, {
|
|
473
|
+
collection,
|
|
474
|
+
isJunction
|
|
475
|
+
}] of allTablesToGenerate.entries()) {
|
|
476
|
+
const tableVarName = getTableVarName(tableName);
|
|
477
|
+
const tableRelations: string[] = [];
|
|
478
|
+
|
|
479
|
+
if (isJunction) {
|
|
480
|
+
const relationInfo = Array.from(allTablesToGenerate.values()).find(v => v.isJunction && getTableName(v.collection) === tableName);
|
|
481
|
+
if (relationInfo && relationInfo.relation && relationInfo.sourceCollection && relationInfo.relation.through) {
|
|
482
|
+
const {
|
|
483
|
+
relation,
|
|
484
|
+
sourceCollection
|
|
485
|
+
} = relationInfo;
|
|
486
|
+
const targetCollection = relation.target();
|
|
487
|
+
const sourceTableVar = getTableVarName(getTableName(sourceCollection));
|
|
488
|
+
const targetTableVar = getTableVarName(getTableName(targetCollection));
|
|
489
|
+
const sourceId = getPrimaryKeyName(sourceCollection);
|
|
490
|
+
const targetId = getPrimaryKeyName(targetCollection);
|
|
491
|
+
|
|
492
|
+
if (!relation?.through)
|
|
493
|
+
throw new Error("Internal, the relation should have a through property. Relations passed to this script should sanitized first with sanitizeRelation().");
|
|
494
|
+
|
|
495
|
+
// The owning relation's name — used on the source side of the junction
|
|
496
|
+
const owningRelationName = relation.relationName ?? toSnakeCase(getTableName(targetCollection));
|
|
497
|
+
|
|
498
|
+
// Find the inverse relation name on the target collection (if any)
|
|
499
|
+
// This is needed so the junction's target-side one() can pair with the
|
|
500
|
+
// inverse many() on the target table.
|
|
501
|
+
let inverseRelationName: string | null = null;
|
|
502
|
+
try {
|
|
503
|
+
const targetRelations = resolveCollectionRelations(targetCollection as import("@rebasepro/types").PostgresCollection<any, any>);
|
|
504
|
+
for (const [, targetRel] of Object.entries(targetRelations)) {
|
|
505
|
+
if (targetRel.direction === "inverse" &&
|
|
506
|
+
targetRel.cardinality === "many" &&
|
|
507
|
+
targetRel.inverseRelationName === owningRelationName) {
|
|
508
|
+
inverseRelationName = targetRel.relationName ?? null;
|
|
509
|
+
break;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
} catch {
|
|
513
|
+
// ignore — inverse side may not exist
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Source side one(): pairs with owning table's many(junctionTable, { relationName })
|
|
517
|
+
tableRelations.push(` ${relation.through.sourceColumn}: one(${sourceTableVar}, {\n fields: [${tableVarName}.${relation.through.sourceColumn}],\n references: [${sourceTableVar}.${sourceId}],\n relationName: \"${owningRelationName}\"\n })`);
|
|
518
|
+
|
|
519
|
+
// Target side one(): pairs with inverse table's many(junctionTable, { relationName })
|
|
520
|
+
const targetRelName = inverseRelationName ?? owningRelationName;
|
|
521
|
+
tableRelations.push(` ${relation.through.targetColumn}: one(${targetTableVar}, {\n fields: [${tableVarName}.${relation.through.targetColumn}],\n references: [${targetTableVar}.${targetId}],\n relationName: \"${targetRelName}\"\n })`);
|
|
522
|
+
}
|
|
523
|
+
} else {
|
|
524
|
+
const resolvedRelations = resolveCollectionRelations(collection as import("@rebasepro/types").PostgresCollection<any, any>);
|
|
525
|
+
for (const [relationKey, rel] of Object.entries(resolvedRelations)) {
|
|
526
|
+
try {
|
|
527
|
+
const target = rel.target();
|
|
528
|
+
const targetTableVar = getTableVarName(getTableName(target));
|
|
529
|
+
const relationName = rel.relationName ?? relationKey;
|
|
530
|
+
|
|
531
|
+
// Determine the correct relation name for Drizzle
|
|
532
|
+
// For inverse relations, we should use the current relation's name, not the inverse name
|
|
533
|
+
const drizzleRelationName = relationName;
|
|
534
|
+
|
|
535
|
+
if (rel.cardinality === "one") {
|
|
536
|
+
if (rel.direction === "owning" && rel.localKey) {
|
|
537
|
+
tableRelations.push(` ${relationKey}: one(${targetTableVar}, {\n fields: [${tableVarName}.${rel.localKey}],\n references: [${targetTableVar}.${getPrimaryKeyName(target)}],\n relationName: \"${drizzleRelationName}\"\n })`);
|
|
538
|
+
} else if (rel.direction === "inverse" && rel.foreignKeyOnTarget) {
|
|
539
|
+
const sourceIdField = getPrimaryKeyName(collection);
|
|
540
|
+
tableRelations.push(` ${relationKey}: one(${targetTableVar}, {\n fields: [${tableVarName}.${sourceIdField}],\n references: [${targetTableVar}.${rel.foreignKeyOnTarget}],\n relationName: \"${drizzleRelationName}\"\n })`);
|
|
541
|
+
} else if (rel.direction === "inverse" && !rel.foreignKeyOnTarget) {
|
|
542
|
+
// Handle inverse one-to-one relations where the FK is on the target table
|
|
543
|
+
// but foreignKeyOnTarget is not explicitly specified
|
|
544
|
+
// In this case, we need to find the corresponding owning relation on the target
|
|
545
|
+
try {
|
|
546
|
+
const targetCollection = rel.target();
|
|
547
|
+
const targetResolvedRelations = resolveCollectionRelations(targetCollection as import("@rebasepro/types").PostgresCollection<any, any>);
|
|
548
|
+
|
|
549
|
+
// Find the owning relation on the target that points back to this collection
|
|
550
|
+
const correspondingRelation = Object.values(targetResolvedRelations).find(targetRel =>
|
|
551
|
+
targetRel.direction === "owning" &&
|
|
552
|
+
targetRel.cardinality === "one" &&
|
|
553
|
+
targetRel.target().slug === collection.slug
|
|
554
|
+
);
|
|
555
|
+
|
|
556
|
+
if (correspondingRelation && correspondingRelation.localKey) {
|
|
557
|
+
const sourceIdField = getPrimaryKeyName(collection);
|
|
558
|
+
tableRelations.push(` ${relationKey}: one(${targetTableVar}, {\n fields: [${tableVarName}.${sourceIdField}],\n references: [${targetTableVar}.${correspondingRelation.localKey}],\n relationName: \"${drizzleRelationName}\"\n })`);
|
|
559
|
+
}
|
|
560
|
+
} catch (e) {
|
|
561
|
+
console.warn(`Could not resolve inverse one-to-one relation '${relationKey}':`, e);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
} else if (rel.cardinality === "many") {
|
|
565
|
+
if (rel.direction === "inverse" && rel.foreignKeyOnTarget) {
|
|
566
|
+
// One-to-many inverse relation
|
|
567
|
+
tableRelations.push(` ${relationKey}: many(${targetTableVar}, { relationName: \"${drizzleRelationName}\" })`);
|
|
568
|
+
} else if (rel.through) {
|
|
569
|
+
// Many-to-many owning relation with explicit junction table
|
|
570
|
+
const junctionTableVar = getTableVarName(rel.through.table);
|
|
571
|
+
tableRelations.push(` ${relationKey}: many(${junctionTableVar}, { relationName: \"${drizzleRelationName}\" })`);
|
|
572
|
+
} else if (rel.direction === "inverse" && rel.inverseRelationName) {
|
|
573
|
+
// Many-to-many inverse relation - find the corresponding owning relation's junction table
|
|
574
|
+
try {
|
|
575
|
+
const targetCollection = rel.target();
|
|
576
|
+
const targetResolvedRelations = resolveCollectionRelations(targetCollection as import("@rebasepro/types").PostgresCollection<any, any>);
|
|
577
|
+
|
|
578
|
+
// Find the corresponding owning many-to-many relation on the target
|
|
579
|
+
const correspondingRelation = Object.values(targetResolvedRelations).find(targetRel =>
|
|
580
|
+
targetRel.direction === "owning" &&
|
|
581
|
+
targetRel.cardinality === "many" &&
|
|
582
|
+
targetRel.through &&
|
|
583
|
+
targetRel.relationName === rel.inverseRelationName
|
|
584
|
+
);
|
|
585
|
+
|
|
586
|
+
if (correspondingRelation && correspondingRelation.through) {
|
|
587
|
+
const junctionTableVar = getTableVarName(correspondingRelation.through.table);
|
|
588
|
+
tableRelations.push(` ${relationKey}: many(${junctionTableVar}, { relationName: \"${drizzleRelationName}\" })`);
|
|
589
|
+
} else {
|
|
590
|
+
console.warn(`Could not find corresponding owning many-to-many relation for inverse relation '${relationKey}' on '${collection.name}'`);
|
|
591
|
+
}
|
|
592
|
+
} catch (e) {
|
|
593
|
+
console.warn(`Could not resolve inverse many-to-many relation '${relationKey}':`, e);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
// joinPath relations don't generate Drizzle relations - they use existing user tables
|
|
597
|
+
}
|
|
598
|
+
} catch (e) {
|
|
599
|
+
console.warn(`Could not generate relation ${relationKey} for ${collection.name}:`, e);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (tableRelations.length > 0) {
|
|
605
|
+
const relVarName = `${tableVarName}Relations`;
|
|
606
|
+
schemaContent += `export const ${relVarName} = drizzleRelations(${tableVarName}, ({ one, many }) => ({\n${tableRelations.join(",\n")}\n}));\n\n`;
|
|
607
|
+
if (!exportedRelationVars.includes(relVarName)) exportedRelationVars.push(relVarName);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// <<< ADDED: Final aggregated exports block
|
|
612
|
+
const tablesExport = `export const tables = { ${exportedTableVars.join(", ")} };\n`;
|
|
613
|
+
const enumsExport = `export const enums = { ${exportedEnumVars.join(", ")} };\n`;
|
|
614
|
+
const relationsExport = `export const relations = { ${exportedRelationVars.join(", ")} };\n\n`;
|
|
615
|
+
schemaContent += tablesExport + enumsExport + relationsExport;
|
|
616
|
+
|
|
617
|
+
return schemaContent;
|
|
618
|
+
};
|