@rebasepro/server-postgresql 0.1.2 → 0.2.3
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 +22 -6
- package/dist/common/src/data/query_builder.d.ts +51 -0
- package/dist/common/src/index.d.ts +1 -0
- package/dist/common/src/util/entities.d.ts +2 -2
- package/dist/common/src/util/relations.d.ts +1 -1
- package/dist/index.es.js +1435 -738
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +1433 -736
- package/dist/index.umd.js.map +1 -1
- package/dist/server-postgresql/src/PostgresAdapter.d.ts +6 -0
- package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +2 -1
- package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +0 -5
- package/dist/server-postgresql/src/auth/ensure-tables.d.ts +2 -1
- package/dist/server-postgresql/src/auth/services.d.ts +37 -15
- package/dist/server-postgresql/src/index.d.ts +1 -0
- package/dist/server-postgresql/src/schema/auth-schema.d.ts +43 -856
- package/dist/server-postgresql/src/schema/default-collections.d.ts +2 -0
- package/dist/server-postgresql/src/schema/doctor.d.ts +10 -1
- package/dist/server-postgresql/src/schema/introspect-db-logic.d.ts +1 -0
- package/dist/server-postgresql/src/services/entity-helpers.d.ts +1 -1
- package/dist/server-postgresql/src/services/realtimeService.d.ts +12 -0
- package/dist/server-postgresql/src/websocket.d.ts +2 -1
- package/dist/types/src/controllers/auth.d.ts +9 -8
- package/dist/types/src/controllers/client.d.ts +3 -0
- package/dist/types/src/controllers/data.d.ts +21 -0
- package/dist/types/src/types/auth_adapter.d.ts +356 -0
- package/dist/types/src/types/collections.d.ts +67 -2
- package/dist/types/src/types/database_adapter.d.ts +94 -0
- package/dist/types/src/types/entity_actions.d.ts +7 -1
- package/dist/types/src/types/entity_callbacks.d.ts +1 -1
- package/dist/types/src/types/entity_views.d.ts +36 -1
- package/dist/types/src/types/index.d.ts +2 -0
- package/dist/types/src/types/plugins.d.ts +1 -1
- package/dist/types/src/types/properties.d.ts +24 -5
- package/dist/types/src/types/property_config.d.ts +6 -2
- package/dist/types/src/types/relations.d.ts +1 -1
- package/dist/types/src/types/translations.d.ts +8 -0
- package/dist/types/src/users/user.d.ts +5 -0
- package/package.json +22 -15
- package/src/PostgresAdapter.ts +59 -0
- package/src/PostgresBackendDriver.ts +66 -13
- package/src/PostgresBootstrapper.ts +35 -15
- package/src/auth/ensure-tables.ts +82 -189
- package/src/auth/services.ts +421 -170
- package/src/cli.ts +49 -13
- package/src/data-transformer.ts +78 -8
- package/src/history/HistoryService.ts +25 -2
- package/src/index.ts +1 -0
- package/src/schema/auth-schema.ts +130 -98
- package/src/schema/default-collections.ts +69 -0
- package/src/schema/doctor-cli.ts +5 -1
- package/src/schema/doctor.ts +166 -48
- package/src/schema/generate-drizzle-schema-logic.ts +74 -27
- package/src/schema/generate-drizzle-schema.ts +13 -3
- package/src/schema/introspect-db-inference.ts +5 -5
- package/src/schema/introspect-db-logic.ts +9 -2
- package/src/schema/introspect-db.ts +14 -3
- package/src/services/EntityFetchService.ts +5 -5
- package/src/services/RelationService.ts +2 -2
- package/src/services/entity-helpers.ts +1 -1
- package/src/services/realtimeService.ts +145 -136
- package/src/utils/drizzle-conditions.ts +16 -2
- package/src/websocket.ts +113 -37
- package/test/auth-services.test.ts +163 -74
- package/test/data-transformer-hardening.test.ts +57 -0
- package/test/data-transformer.test.ts +43 -0
- package/test/generate-drizzle-schema.test.ts +7 -5
- package/test/introspect-db-utils.test.ts +4 -1
- package/test/postgresDataDriver.test.ts +147 -1
- package/test/realtimeService.test.ts +7 -7
- package/test/websocket.test.ts +139 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EntityCollection, NumberProperty, Property, Relation, RelationProperty, SecurityOperation, SecurityRule, StringProperty, isPostgresCollection, DateProperty, ArrayProperty, MapProperty, ReferenceProperty } from "@rebasepro/types";
|
|
1
|
+
import { EntityCollection, NumberProperty, Property, Relation, RelationProperty, SecurityOperation, SecurityRule, StringProperty, isPostgresCollection, DateProperty, ArrayProperty, MapProperty, ReferenceProperty, VectorProperty, BinaryProperty } from "@rebasepro/types";
|
|
2
2
|
import { getPrimaryKeys } from "../services/entity-helpers";
|
|
3
3
|
import { getEnumVarName, getTableName, getTableVarName, resolveCollectionRelations, findRelation } from "@rebasepro/common";
|
|
4
4
|
import { toSnakeCase } from "@rebasepro/utils";
|
|
@@ -19,23 +19,23 @@ const resolveColumnName = (propName: string, prop?: Property | null): string =>
|
|
|
19
19
|
|
|
20
20
|
const getPrimaryKeyProp = (collection: EntityCollection): { name: string, type: "string" | "number", isUuid: boolean } => {
|
|
21
21
|
if (collection.properties) {
|
|
22
|
-
const idPropEntry = Object.entries(collection.properties).find(([_, prop]) => "isId" in (prop as object) && Boolean((prop as unknown as Record<string, unknown>).isId));
|
|
22
|
+
const idPropEntry = Object.entries(collection.properties).find(([_, prop]) => "isId" in (prop as unknown as object) && Boolean((prop as unknown as Record<string, unknown>).isId));
|
|
23
23
|
if (idPropEntry) {
|
|
24
|
-
const prop = idPropEntry[1] as Property;
|
|
25
|
-
const isUuid = prop.type === "string" && "isId" in prop && (prop as StringProperty).isId === "uuid";
|
|
24
|
+
const prop = idPropEntry[1] as unknown as Property;
|
|
25
|
+
const isUuid = prop.type === "string" && "isId" in prop && (prop as unknown as StringProperty).isId === "uuid";
|
|
26
26
|
return { name: idPropEntry[0],
|
|
27
27
|
type: prop.type === "number" ? "number" : "string",
|
|
28
28
|
isUuid };
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
// Fallback
|
|
32
|
-
const idProp = collection.properties?.["id"] as Property | undefined;
|
|
32
|
+
const idProp = collection.properties?.["id"] as unknown as Property | undefined;
|
|
33
33
|
if (idProp?.type === "number") {
|
|
34
34
|
return { name: "id",
|
|
35
35
|
type: "number",
|
|
36
36
|
isUuid: false };
|
|
37
37
|
}
|
|
38
|
-
const isUuid = idProp?.type === "string" && "isId" in idProp && (idProp as StringProperty).isId === "uuid";
|
|
38
|
+
const isUuid = idProp?.type === "string" && "isId" in idProp && (idProp as unknown as StringProperty).isId === "uuid";
|
|
39
39
|
return { name: "id",
|
|
40
40
|
type: "string",
|
|
41
41
|
isUuid: isUuid ?? false };
|
|
@@ -53,17 +53,18 @@ const isIdProperty = (propName: string, prop: Property, collection: EntityCollec
|
|
|
53
53
|
if ("isId" in prop && Boolean(prop.isId)) return true;
|
|
54
54
|
|
|
55
55
|
// We only fallback to "id" if NO property is explicitly marked with `isId: true` or a generator string
|
|
56
|
-
const hasExplicitId = Object.values(collection.properties ?? {}).some(p => "isId" in (p as object) && Boolean((p as unknown as Record<string, unknown>).isId));
|
|
56
|
+
const hasExplicitId = Object.values(collection.properties ?? {}).some(p => "isId" in (p as unknown as object) && Boolean((p as unknown as Record<string, unknown>).isId));
|
|
57
57
|
return !hasExplicitId && propName === "id";
|
|
58
58
|
};
|
|
59
59
|
|
|
60
60
|
const getDrizzleColumn = (propName: string, prop: Property, collection: EntityCollection, collections: EntityCollection[]): string | null => {
|
|
61
|
+
|
|
61
62
|
const colName = resolveColumnName(propName, prop);
|
|
62
63
|
let columnDefinition: string;
|
|
63
64
|
|
|
64
65
|
switch (prop.type) {
|
|
65
66
|
case "string": {
|
|
66
|
-
const stringProp = prop as StringProperty;
|
|
67
|
+
const stringProp = prop as unknown as StringProperty;
|
|
67
68
|
if (stringProp.enum) {
|
|
68
69
|
const enumName = getEnumVarName(getTableName(collection), propName);
|
|
69
70
|
columnDefinition = `${enumName}("${colName}")`;
|
|
@@ -97,7 +98,7 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: EntityCo
|
|
|
97
98
|
break;
|
|
98
99
|
}
|
|
99
100
|
case "number": {
|
|
100
|
-
const numProp = prop as NumberProperty;
|
|
101
|
+
const numProp = prop as unknown as NumberProperty;
|
|
101
102
|
const isId = isIdProperty(propName, prop, collection);
|
|
102
103
|
|
|
103
104
|
let baseType = (numProp.validation?.integer || isId) ? `integer("${colName}")` : `numeric("${colName}")`;
|
|
@@ -154,6 +155,15 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: EntityCo
|
|
|
154
155
|
}
|
|
155
156
|
break;
|
|
156
157
|
}
|
|
158
|
+
case "vector": {
|
|
159
|
+
const vp = prop as VectorProperty;
|
|
160
|
+
columnDefinition = `vector("${colName}", { dimensions: ${vp.dimensions} })`;
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
case "binary": {
|
|
164
|
+
columnDefinition = `customType({ dataType() { return 'bytea'; } })("${colName}")`;
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
157
167
|
case "relation": {
|
|
158
168
|
const refProp = prop as RelationProperty;
|
|
159
169
|
const resolvedRelations = resolveCollectionRelations(collection);
|
|
@@ -245,8 +255,8 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: EntityCo
|
|
|
245
255
|
* The result is wrapped in a Drizzle sql`` template literal.
|
|
246
256
|
*/
|
|
247
257
|
const resolveRawSql = (expression: string): string => {
|
|
248
|
-
// Replace {column_name} with
|
|
249
|
-
const resolved = expression.replace(/\{(\w+)\}/g, (_, col) =>
|
|
258
|
+
// Replace {column_name} with column_name directly (so Drizzle-kit can parse it as a static string)
|
|
259
|
+
const resolved = expression.replace(/\{(\w+)\}/g, (_, col) => col);
|
|
250
260
|
return `sql\`${resolved}\``;
|
|
251
261
|
};
|
|
252
262
|
|
|
@@ -271,7 +281,7 @@ const unwrapSql = (sqlExpr: string): string => {
|
|
|
271
281
|
/**
|
|
272
282
|
* Builds the USING clause for a policy based on shortcuts or raw SQL.
|
|
273
283
|
*/
|
|
274
|
-
const buildUsingClause = (rule: SecurityRule): string | null => {
|
|
284
|
+
const buildUsingClause = (rule: SecurityRule, collection: EntityCollection): string | null => {
|
|
275
285
|
if (rule.using) {
|
|
276
286
|
return resolveRawSql(rule.using);
|
|
277
287
|
}
|
|
@@ -279,7 +289,9 @@ const buildUsingClause = (rule: SecurityRule): string | null => {
|
|
|
279
289
|
return "sql`true`";
|
|
280
290
|
}
|
|
281
291
|
if (rule.ownerField) {
|
|
282
|
-
|
|
292
|
+
const prop = collection.properties?.[rule.ownerField];
|
|
293
|
+
const colName = resolveColumnName(rule.ownerField, prop);
|
|
294
|
+
return `sql\`${colName} = auth.uid()\``;
|
|
283
295
|
}
|
|
284
296
|
return null;
|
|
285
297
|
};
|
|
@@ -288,12 +300,12 @@ const buildUsingClause = (rule: SecurityRule): string | null => {
|
|
|
288
300
|
* Builds the WITH CHECK clause for a policy based on shortcuts or raw SQL.
|
|
289
301
|
* Falls back to the USING clause if not explicitly provided.
|
|
290
302
|
*/
|
|
291
|
-
const buildWithCheckClause = (rule: SecurityRule): string | null => {
|
|
303
|
+
const buildWithCheckClause = (rule: SecurityRule, collection: EntityCollection): string | null => {
|
|
292
304
|
if (rule.withCheck) {
|
|
293
305
|
return resolveRawSql(rule.withCheck);
|
|
294
306
|
}
|
|
295
307
|
// For insert/update/all, fall back to using clause if withCheck not specified
|
|
296
|
-
return buildUsingClause(rule);
|
|
308
|
+
return buildUsingClause(rule, collection);
|
|
297
309
|
};
|
|
298
310
|
|
|
299
311
|
/**
|
|
@@ -324,7 +336,8 @@ const getPolicyNameHash = (rule: SecurityRule): string => {
|
|
|
324
336
|
* - operations[] array: generates one policy per operation
|
|
325
337
|
* - Combinations: roles + ownerField, roles + raw SQL, etc.
|
|
326
338
|
*/
|
|
327
|
-
const generatePolicyCode = (
|
|
339
|
+
const generatePolicyCode = (collection: EntityCollection, rule: SecurityRule, index: number): string => {
|
|
340
|
+
const tableName = getTableName(collection);
|
|
328
341
|
// Resolve operations: operations[] takes precedence over operation (singular)
|
|
329
342
|
const ops: SecurityOperation[] = rule.operations && rule.operations.length > 0
|
|
330
343
|
? rule.operations
|
|
@@ -338,14 +351,14 @@ const generatePolicyCode = (tableName: string, rule: SecurityRule, index: number
|
|
|
338
351
|
? (ops.length > 1 ? `${rule.name}_${op}` : rule.name)
|
|
339
352
|
: `${tableName}_${op}_${ruleHash}${ops.length > 1 ? `_${opIdx}` : ""}`;
|
|
340
353
|
|
|
341
|
-
return generateSinglePolicyCode(
|
|
354
|
+
return generateSinglePolicyCode(collection, rule, op, policyName);
|
|
342
355
|
}).join("");
|
|
343
356
|
};
|
|
344
357
|
|
|
345
358
|
/**
|
|
346
359
|
* Generates a single pgPolicy() call for one specific operation.
|
|
347
360
|
*/
|
|
348
|
-
const generateSinglePolicyCode = (
|
|
361
|
+
const generateSinglePolicyCode = (collection: EntityCollection, rule: SecurityRule, operation: SecurityOperation, policyName: string): string => {
|
|
349
362
|
const mode = rule.mode ?? "permissive";
|
|
350
363
|
const roles = rule.roles ? [...rule.roles].sort() : undefined;
|
|
351
364
|
|
|
@@ -356,8 +369,8 @@ const generateSinglePolicyCode = (tableName: string, rule: SecurityRule, operati
|
|
|
356
369
|
const needsUsing = operation !== "insert";
|
|
357
370
|
const needsWithCheck = operation !== "select" && operation !== "delete";
|
|
358
371
|
|
|
359
|
-
let usingClause = needsUsing ? buildUsingClause(rule) : null;
|
|
360
|
-
let withCheckClause = needsWithCheck ? buildWithCheckClause(rule) : null;
|
|
372
|
+
let usingClause = needsUsing ? buildUsingClause(rule, collection) : null;
|
|
373
|
+
let withCheckClause = needsWithCheck ? buildWithCheckClause(rule, collection) : null;
|
|
361
374
|
|
|
362
375
|
// If roles are specified, wrap existing clauses with role check,
|
|
363
376
|
// or generate a roles-only clause.
|
|
@@ -485,18 +498,41 @@ export const generateSchema = async (collections: EntityCollection[], stripPolic
|
|
|
485
498
|
)
|
|
486
499
|
);
|
|
487
500
|
|
|
488
|
-
const
|
|
501
|
+
const hasVector = collections.some(c =>
|
|
489
502
|
c.properties && Object.values(c.properties).some(
|
|
490
|
-
(p: Property) =>
|
|
503
|
+
(p: Property) => p.type === "vector"
|
|
504
|
+
)
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
const hasBinary = collections.some(c =>
|
|
508
|
+
c.properties && Object.values(c.properties).some(
|
|
509
|
+
(p: Property) => p.type === "binary"
|
|
491
510
|
)
|
|
492
511
|
);
|
|
493
512
|
|
|
494
513
|
// Always import pgPolicy and sql — RLS is enabled on every table (secure by default)
|
|
495
514
|
const pgCoreImports = ["primaryKey", "pgTable", "integer", "varchar", "text", "char", "boolean", "timestamp", "date", "time", "jsonb", "json", "pgEnum", "numeric", "real", "doublePrecision", "bigint", "serial", "bigserial", "pgPolicy"];
|
|
496
515
|
if (hasUuid) pgCoreImports.push("uuid");
|
|
516
|
+
if (hasVector) pgCoreImports.push("vector");
|
|
517
|
+
if (hasBinary) pgCoreImports.push("customType");
|
|
518
|
+
|
|
519
|
+
const uniqueSchemas = Array.from(new Set(
|
|
520
|
+
collections.map(c => isPostgresCollection(c) ? c.schema : undefined).filter(Boolean)
|
|
521
|
+
));
|
|
522
|
+
if (uniqueSchemas.length > 0) {
|
|
523
|
+
pgCoreImports.push("pgSchema");
|
|
524
|
+
}
|
|
525
|
+
|
|
497
526
|
schemaContent += `import { ${pgCoreImports.join(", ")} } from 'drizzle-orm/pg-core';\n`;
|
|
498
527
|
schemaContent += "import { relations as drizzleRelations, sql } from 'drizzle-orm';\n\n";
|
|
499
528
|
|
|
529
|
+
uniqueSchemas.forEach(schema => {
|
|
530
|
+
schemaContent += `export const ${schema}Schema = pgSchema("${schema}");\n`;
|
|
531
|
+
});
|
|
532
|
+
if (uniqueSchemas.length > 0) {
|
|
533
|
+
schemaContent += "\n";
|
|
534
|
+
}
|
|
535
|
+
|
|
500
536
|
const exportedTableVars: string[] = [];
|
|
501
537
|
const exportedEnumVars: string[] = [];
|
|
502
538
|
const exportedRelationVars: string[] = [];
|
|
@@ -512,6 +548,7 @@ export const generateSchema = async (collections: EntityCollection[], stripPolic
|
|
|
512
548
|
collections.forEach(collection => {
|
|
513
549
|
const collectionPath = getTableName(collection);
|
|
514
550
|
Object.entries(collection.properties ?? {}).forEach(([propName, prop]) => {
|
|
551
|
+
|
|
515
552
|
if (("enum" in prop) && (prop.type === "string" || prop.type === "number") && prop.enum) {
|
|
516
553
|
const enumVarName = getEnumVarName(collectionPath, propName);
|
|
517
554
|
const enumDbName = `${collectionPath}_${resolveColumnName(propName, prop)}`;
|
|
@@ -564,6 +601,9 @@ export const generateSchema = async (collections: EntityCollection[], stripPolic
|
|
|
564
601
|
const tableVarName = getTableVarName(tableName);
|
|
565
602
|
if (isJunction && relation && sourceCollection && relation.through) {
|
|
566
603
|
const targetCollection = relation.target();
|
|
604
|
+
const schema = (isPostgresCollection(targetCollection) ? targetCollection.schema : undefined) || (isPostgresCollection(sourceCollection) ? sourceCollection.schema : undefined);
|
|
605
|
+
const tableCreator = schema ? `${schema}Schema.table` : "pgTable";
|
|
606
|
+
const baseTableName = tableName.includes(".") ? tableName.split(".").pop()! : tableName;
|
|
567
607
|
const {
|
|
568
608
|
sourceColumn,
|
|
569
609
|
targetColumn
|
|
@@ -577,14 +617,17 @@ export const generateSchema = async (collections: EntityCollection[], stripPolic
|
|
|
577
617
|
const sourceId = getPrimaryKeyName(sourceCollection);
|
|
578
618
|
const targetId = getPrimaryKeyName(targetCollection);
|
|
579
619
|
|
|
580
|
-
schemaContent += `export const ${tableVarName} =
|
|
620
|
+
schemaContent += `export const ${tableVarName} = ${tableCreator}(\"${baseTableName}\", {\n`;
|
|
581
621
|
schemaContent += ` ${sourceColumn}: ${sourceColType}(\"${sourceColumn}\").notNull().references(() => ${getTableVarName(getTableName(sourceCollection))}.${sourceId}, ${refOptions}),\n`;
|
|
582
622
|
schemaContent += ` ${targetColumn}: ${targetColType}(\"${targetColumn}\").notNull().references(() => ${getTableVarName(getTableName(targetCollection))}.${targetId}, ${refOptions}),\n`;
|
|
583
623
|
schemaContent += "}, (table) => ({\n";
|
|
584
624
|
schemaContent += ` pk: primaryKey({ columns: [table.${sourceColumn}, table.${targetColumn}] })\n`;
|
|
585
625
|
schemaContent += "}));\n\n";
|
|
586
626
|
} else if (!isJunction) {
|
|
587
|
-
|
|
627
|
+
const schema = isPostgresCollection(collection) ? collection.schema : undefined;
|
|
628
|
+
const tableCreator = schema ? `${schema}Schema.table` : "pgTable";
|
|
629
|
+
const baseTableName = tableName.includes(".") ? tableName.split(".").pop()! : tableName;
|
|
630
|
+
schemaContent += `export const ${tableVarName} = ${tableCreator}(\"${baseTableName}\", {\n`;
|
|
588
631
|
const columns = new Set<string>();
|
|
589
632
|
Object.entries(collection.properties ?? {}).forEach(([propName, prop]) => {
|
|
590
633
|
const columnString = getDrizzleColumn(propName, prop as Property, collection, collections);
|
|
@@ -605,7 +648,7 @@ export const generateSchema = async (collections: EntityCollection[], stripPolic
|
|
|
605
648
|
if (!stripPolicies && securityRules && securityRules.length > 0) {
|
|
606
649
|
schemaContent += "\n}, (table) => ([\n";
|
|
607
650
|
securityRules.forEach((rule: SecurityRule, idx: number) => {
|
|
608
|
-
schemaContent += generatePolicyCode(
|
|
651
|
+
schemaContent += generatePolicyCode(collection, rule, idx);
|
|
609
652
|
});
|
|
610
653
|
schemaContent += "])).enableRLS();\n\n";
|
|
611
654
|
} else {
|
|
@@ -666,8 +709,12 @@ export const generateSchema = async (collections: EntityCollection[], stripPolic
|
|
|
666
709
|
tableRelations.push(` "${relation.through.sourceColumn}": one(${sourceTableVar}, {\n fields: [${tableVarName}.${relation.through.sourceColumn}],\n references: [${sourceTableVar}.${sourceId}],\n relationName: \"${owningRelationName}\"\n })`);
|
|
667
710
|
|
|
668
711
|
// Target side one(): pairs with inverse table's many(junctionTable, { relationName })
|
|
669
|
-
|
|
670
|
-
|
|
712
|
+
// Always emit a relationName to avoid collisions with the source-side's owningRelationName.
|
|
713
|
+
// When no inverse relation exists on the target collection, synthesize a unique name.
|
|
714
|
+
const targetRelationName = inverseRelationName
|
|
715
|
+
? inverseRelationName
|
|
716
|
+
: `${tableName}_${relation.through.targetColumn}`;
|
|
717
|
+
tableRelations.push(` "${relation.through.targetColumn}": one(${targetTableVar}, {\n fields: [${tableVarName}.${relation.through.targetColumn}],\n references: [${targetTableVar}.${targetId}],\n relationName: "${targetRelationName}"\n })`);
|
|
671
718
|
}
|
|
672
719
|
} else {
|
|
673
720
|
const resolvedRelations = resolveCollectionRelations(collection);
|
|
@@ -5,6 +5,7 @@ import { pathToFileURL } from "url";
|
|
|
5
5
|
import chokidar from "chokidar";
|
|
6
6
|
import { generateSchema } from "./generate-drizzle-schema-logic";
|
|
7
7
|
import { EntityCollection } from "@rebasepro/types";
|
|
8
|
+
import { defaultUsersCollection } from "./default-collections";
|
|
8
9
|
|
|
9
10
|
// --- Helper Functions ---
|
|
10
11
|
|
|
@@ -84,11 +85,20 @@ const runGeneration = async (collectionsFilePath?: string, outputPath?: string)
|
|
|
84
85
|
collections = imported.backendCollections || imported.collections;
|
|
85
86
|
}
|
|
86
87
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
// If collections directory is empty but exists, or failed to find any, we still want to inject defaults
|
|
89
|
+
if (!collections || !Array.isArray(collections)) {
|
|
90
|
+
collections = [];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Inject default collections if not overridden by the developer
|
|
94
|
+
const hasUsersCollection = collections.some(c => c.slug === "users");
|
|
95
|
+
if (!hasUsersCollection) {
|
|
96
|
+
collections.push(defaultUsersCollection);
|
|
90
97
|
}
|
|
91
98
|
|
|
99
|
+
// Sort collections by slug alphabetically to ensure deterministic schema generation
|
|
100
|
+
collections.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
101
|
+
|
|
92
102
|
const schemaContent = await generateSchema(collections);
|
|
93
103
|
|
|
94
104
|
if (outputPath) {
|
|
@@ -17,8 +17,8 @@ export function inferPropertyFromData(
|
|
|
17
17
|
sampleValues: unknown[],
|
|
18
18
|
isPk: boolean
|
|
19
19
|
): InferenceResult {
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
const result: InferenceResult = {};
|
|
21
|
+
const extraLines: string[] = [];
|
|
22
22
|
|
|
23
23
|
// Filter out null/undefined for analysis
|
|
24
24
|
const validValues = sampleValues.filter(v => v !== null && v !== undefined && v !== "");
|
|
@@ -84,7 +84,7 @@ export function inferPropertyFromData(
|
|
|
84
84
|
let allNumbers = true;
|
|
85
85
|
let allStrings = true;
|
|
86
86
|
for (const v of validValues) {
|
|
87
|
-
|
|
87
|
+
const parsed = typeof v === "string" ? JSON.parse(v) : v;
|
|
88
88
|
for (const item of parsed) {
|
|
89
89
|
if (typeof item !== "number") allNumbers = false;
|
|
90
90
|
if (typeof item !== "string") allStrings = false;
|
|
@@ -99,7 +99,7 @@ export function inferPropertyFromData(
|
|
|
99
99
|
if (allObjects && validValues.length > 0) {
|
|
100
100
|
const schema: Record<string, string> = {};
|
|
101
101
|
for (const v of validValues) {
|
|
102
|
-
|
|
102
|
+
const parsed = typeof v === "string" ? JSON.parse(v) : v;
|
|
103
103
|
for (const [k, val] of Object.entries(parsed)) {
|
|
104
104
|
if (val === null || val === undefined) continue;
|
|
105
105
|
const type = typeof val;
|
|
@@ -221,7 +221,7 @@ export function inferPropertyFromData(
|
|
|
221
221
|
if (hasFileExtension) {
|
|
222
222
|
const firstVal = validValues[0] as string;
|
|
223
223
|
const lastSlash = firstVal.lastIndexOf('/');
|
|
224
|
-
|
|
224
|
+
const inferredStoragePath = lastSlash > 0 ? firstVal.substring(0, lastSlash) : "files";
|
|
225
225
|
extraLines.push(` storage: {\n storagePath: "${inferredStoragePath}"\n }`);
|
|
226
226
|
} else if (isUrl) {
|
|
227
227
|
if (isMedia) {
|
|
@@ -21,6 +21,7 @@ export interface TableColumn {
|
|
|
21
21
|
udt_name: string;
|
|
22
22
|
is_nullable: string;
|
|
23
23
|
column_default: string | null;
|
|
24
|
+
atttypmod: number | null;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
export interface EnumValue {
|
|
@@ -187,7 +188,7 @@ export function mapPgType(dataType: string): string {
|
|
|
187
188
|
if (dt === "json" || dt === "jsonb") return "map";
|
|
188
189
|
|
|
189
190
|
// Binary
|
|
190
|
-
if (dt === "bytea") return "
|
|
191
|
+
if (dt === "bytea") return "binary";
|
|
191
192
|
|
|
192
193
|
// Network types
|
|
193
194
|
if (dt === "inet" || dt === "cidr" || dt === "macaddr" || dt === "macaddr8") return "string";
|
|
@@ -548,8 +549,9 @@ export function generateCollectionFile(
|
|
|
548
549
|
// Check if this column uses a PostgreSQL enum type
|
|
549
550
|
const colEnumValues = enumMap.get(col.udt_name);
|
|
550
551
|
const isEnumColumn = col.data_type === "USER-DEFINED" && colEnumValues !== undefined;
|
|
552
|
+
const isVectorColumn = col.udt_name === "vector";
|
|
551
553
|
|
|
552
|
-
const propType = isEnumColumn ? "string" : mapPgType(col.data_type);
|
|
554
|
+
const propType = isEnumColumn ? "string" : (isVectorColumn ? "vector" : mapPgType(col.data_type));
|
|
553
555
|
let extra = "";
|
|
554
556
|
|
|
555
557
|
const colNameLower = col.column_name.toLowerCase();
|
|
@@ -633,6 +635,11 @@ export function generateCollectionFile(
|
|
|
633
635
|
}
|
|
634
636
|
}
|
|
635
637
|
|
|
638
|
+
if (finalPropType === "vector") {
|
|
639
|
+
const dims = col.atttypmod && col.atttypmod > 0 ? col.atttypmod : 1536;
|
|
640
|
+
extra += `\n dimensions: ${dims},`;
|
|
641
|
+
}
|
|
642
|
+
|
|
636
643
|
if (col.is_nullable === "NO" && !meta.pks.includes(col.column_name) && !col.column_default) {
|
|
637
644
|
if (extra.includes("validation: {")) {
|
|
638
645
|
extra = extra.replace("validation: {", "validation: {\n required: true,");
|
|
@@ -99,9 +99,20 @@ async function main() {
|
|
|
99
99
|
|
|
100
100
|
// 2. Get Columns
|
|
101
101
|
const { rows: columns } = await client.query<TableColumn>(`
|
|
102
|
-
SELECT
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
SELECT
|
|
103
|
+
c.table_name,
|
|
104
|
+
c.column_name,
|
|
105
|
+
c.data_type,
|
|
106
|
+
c.udt_name,
|
|
107
|
+
c.is_nullable,
|
|
108
|
+
c.column_default,
|
|
109
|
+
(SELECT a.atttypmod FROM pg_attribute a
|
|
110
|
+
JOIN pg_class pc ON a.attrelid = pc.oid
|
|
111
|
+
WHERE pc.relname = c.table_name
|
|
112
|
+
AND a.attname = c.column_name
|
|
113
|
+
AND pc.relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = c.table_schema)) as atttypmod
|
|
114
|
+
FROM information_schema.columns c
|
|
115
|
+
WHERE c.table_schema = $1
|
|
105
116
|
`, [pgSchema]);
|
|
106
117
|
|
|
107
118
|
// 2b. Get Enum Types and their values
|
|
@@ -606,7 +606,7 @@ export class EntityFetchService {
|
|
|
606
606
|
const row = await qb.findFirst({
|
|
607
607
|
where: eq(idField, parsedId),
|
|
608
608
|
with: withConfig
|
|
609
|
-
} as
|
|
609
|
+
} as Parameters<NonNullable<typeof qb>["findFirst"]>[0]);
|
|
610
610
|
|
|
611
611
|
if (!row) return undefined;
|
|
612
612
|
|
|
@@ -729,7 +729,7 @@ export class EntityFetchService {
|
|
|
729
729
|
);
|
|
730
730
|
|
|
731
731
|
|
|
732
|
-
const results = await qb.findMany(queryOpts as
|
|
732
|
+
const results = await qb.findMany(queryOpts as Parameters<NonNullable<typeof qb>["findMany"]>[0]);
|
|
733
733
|
|
|
734
734
|
const entities = (results as Record<string, unknown>[]).map(row =>
|
|
735
735
|
this.drizzleResultToEntity<M>(row, collection, collectionPath, idInfo, options.databaseId, idInfoArray)
|
|
@@ -1192,7 +1192,7 @@ export class EntityFetchService {
|
|
|
1192
1192
|
);
|
|
1193
1193
|
|
|
1194
1194
|
|
|
1195
|
-
const results = await qb.findMany(queryOpts as
|
|
1195
|
+
const results = await qb.findMany(queryOpts as Parameters<NonNullable<typeof qb>["findMany"]>[0]);
|
|
1196
1196
|
|
|
1197
1197
|
const restRows = (results as Record<string, unknown>[]).map(row =>
|
|
1198
1198
|
this.drizzleResultToRestRow(row, collection, idInfo, idInfoArray)
|
|
@@ -1306,7 +1306,7 @@ export class EntityFetchService {
|
|
|
1306
1306
|
const row = await qb.findFirst({
|
|
1307
1307
|
where: eq(idField, parsedId),
|
|
1308
1308
|
...(withConfig ? { with: withConfig } : {})
|
|
1309
|
-
} as
|
|
1309
|
+
} as Parameters<NonNullable<typeof qb>["findFirst"]>[0]);
|
|
1310
1310
|
|
|
1311
1311
|
if (!row) return null;
|
|
1312
1312
|
|
|
@@ -1516,7 +1516,7 @@ export class EntityFetchService {
|
|
|
1516
1516
|
}
|
|
1517
1517
|
|
|
1518
1518
|
|
|
1519
|
-
const results = await queryTarget.findMany(queryOpts as
|
|
1519
|
+
const results = await queryTarget.findMany(queryOpts as Parameters<NonNullable<typeof queryTarget>["findMany"]>[0]);
|
|
1520
1520
|
|
|
1521
1521
|
// Flatten the nested Drizzle results into REST format
|
|
1522
1522
|
return results.map((row: Record<string, unknown>) => {
|
|
@@ -1159,7 +1159,7 @@ export class RelationService {
|
|
|
1159
1159
|
if (parentFKValue !== null && parentFKValue !== undefined) {
|
|
1160
1160
|
await tx.update(targetTable)
|
|
1161
1161
|
.set({ [targetFKColName]: null })
|
|
1162
|
-
.where(eq(targetFKCol, parentFKValue
|
|
1162
|
+
.where(eq(targetFKCol, String(parentFKValue)));
|
|
1163
1163
|
}
|
|
1164
1164
|
continue;
|
|
1165
1165
|
}
|
|
@@ -1172,7 +1172,7 @@ export class RelationService {
|
|
|
1172
1172
|
if (parentFKValue !== null && parentFKValue !== undefined) {
|
|
1173
1173
|
await tx.update(targetTable)
|
|
1174
1174
|
.set({ [targetFKColName]: null })
|
|
1175
|
-
.where(eq(targetFKCol, parentFKValue
|
|
1175
|
+
.where(eq(targetFKCol, String(parentFKValue)));
|
|
1176
1176
|
} else {
|
|
1177
1177
|
console.warn(`Cannot set joinPath relation '${relation.relationName}' because parent FK value is null/undefined`);
|
|
1178
1178
|
continue;
|
|
@@ -13,7 +13,7 @@ import { getTableName } from "@rebasepro/common";
|
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Interface for Drizzle column metadata introspection.
|
|
16
|
-
* Replaces unsafe `as
|
|
16
|
+
* Replaces unsafe `as Record<string, unknown>` double-cast chains.
|
|
17
17
|
*/
|
|
18
18
|
export interface DrizzleColumnMeta {
|
|
19
19
|
columnType?: string;
|