@rebasepro/server-postgresql 0.0.1-canary.892f711 → 0.0.1-canary.a6becfb
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/index.es.js +381 -1074
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +312 -1005
- package/dist/index.umd.js.map +1 -1
- package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +2 -1
- package/dist/server-postgresql/src/schema/introspect-db-inference.d.ts +5 -0
- package/dist/server-postgresql/src/schema/introspect-db-logic.d.ts +44 -9
- package/dist/server-postgresql/src/services/EntityPersistService.d.ts +9 -0
- package/dist/types/src/controllers/auth.d.ts +8 -2
- package/dist/types/src/controllers/client.d.ts +13 -0
- package/dist/types/src/controllers/navigation.d.ts +18 -6
- package/dist/types/src/controllers/registry.d.ts +9 -1
- package/dist/types/src/controllers/side_entity_controller.d.ts +7 -0
- package/dist/types/src/rebase_context.d.ts +17 -0
- package/dist/types/src/types/collections.d.ts +64 -1
- package/dist/types/src/types/component_ref.d.ts +47 -0
- package/dist/types/src/types/entity_views.d.ts +2 -1
- package/dist/types/src/types/index.d.ts +1 -0
- package/dist/types/src/types/properties.d.ts +3 -3
- package/dist/types/src/types/translations.d.ts +8 -0
- package/package.json +5 -5
- package/src/PostgresBackendDriver.ts +23 -6
- package/src/cli.ts +9 -1
- package/src/data-transformer.ts +84 -1
- package/src/schema/generate-drizzle-schema-logic.ts +35 -0
- package/src/schema/introspect-db-inference.ts +238 -0
- package/src/schema/introspect-db-logic.ts +337 -36
- package/src/schema/introspect-db.ts +66 -23
- package/src/services/EntityFetchService.ts +16 -0
- package/src/services/EntityPersistService.ts +88 -12
- package/test/generate-drizzle-schema.test.ts +128 -0
- package/test/introspect-db-generation.test.ts +5 -5
- package/test/property-ordering.test.ts +395 -0
|
@@ -298,51 +298,127 @@ export class EntityPersistService {
|
|
|
298
298
|
|
|
299
299
|
if (pgError) {
|
|
300
300
|
const detail = pgError.detail as string | undefined;
|
|
301
|
+
const hint = pgError.hint as string | undefined;
|
|
301
302
|
const constraint = pgError.constraint as string | undefined;
|
|
302
303
|
const column = pgError.column as string | undefined;
|
|
303
304
|
const table = pgError.table as string | undefined;
|
|
305
|
+
const dataType = pgError.dataType as string | undefined;
|
|
306
|
+
const pgMessage = pgError.message || "Unknown database error";
|
|
307
|
+
|
|
308
|
+
const suffix = hint ? ` Hint: ${hint}` : "";
|
|
309
|
+
const tableRef = table ?? collectionSlug;
|
|
304
310
|
|
|
305
311
|
switch (pgError.code) {
|
|
306
312
|
case "23503": // foreign_key_violation
|
|
307
313
|
return new Error(
|
|
308
314
|
detail
|
|
309
|
-
? `Foreign key constraint violated: ${detail}`
|
|
310
|
-
: `Cannot save: a foreign key constraint${constraint ? ` (${constraint})` : ""} was violated in "${collectionSlug}"
|
|
315
|
+
? `Foreign key constraint violated: ${detail}${suffix}`
|
|
316
|
+
: `Cannot save: a foreign key constraint${constraint ? ` (${constraint})` : ""} was violated in "${collectionSlug}".${suffix}`
|
|
311
317
|
);
|
|
312
318
|
case "23505": // unique_violation
|
|
313
319
|
return new Error(
|
|
314
320
|
detail
|
|
315
|
-
? `Duplicate value: ${detail}`
|
|
316
|
-
: `Cannot save: a unique constraint${constraint ? ` (${constraint})` : ""} was violated in "${collectionSlug}"
|
|
321
|
+
? `Duplicate value: ${detail}${suffix}`
|
|
322
|
+
: `Cannot save: a unique constraint${constraint ? ` (${constraint})` : ""} was violated in "${collectionSlug}".${suffix}`
|
|
317
323
|
);
|
|
318
324
|
case "23502": // not_null_violation
|
|
319
325
|
return new Error(
|
|
320
|
-
`Missing required field: "${column ?? "unknown"}" in "${
|
|
326
|
+
`Missing required field: "${column ?? "unknown"}" in "${tableRef}" cannot be empty.${suffix}`
|
|
321
327
|
);
|
|
322
328
|
case "23514": // check_violation
|
|
323
329
|
return new Error(
|
|
324
|
-
`Validation failed: a check constraint${constraint ? ` (${constraint})` : ""} was violated in "${collectionSlug}"
|
|
330
|
+
`Validation failed: a check constraint${constraint ? ` (${constraint})` : ""} was violated in "${collectionSlug}".${suffix}`
|
|
331
|
+
);
|
|
332
|
+
case "22P02": // invalid_text_representation (e.g. invalid UUID, wrong enum value)
|
|
333
|
+
return new Error(
|
|
334
|
+
`Invalid data format in "${collectionSlug}": ${pgMessage}${suffix}`
|
|
335
|
+
);
|
|
336
|
+
case "22001": // string_data_right_truncation (value too long)
|
|
337
|
+
return new Error(
|
|
338
|
+
`Value too long for column "${column ?? "unknown"}" in "${tableRef}": ${pgMessage}${suffix}`
|
|
339
|
+
);
|
|
340
|
+
case "22003": // numeric_value_out_of_range
|
|
341
|
+
return new Error(
|
|
342
|
+
`Numeric value out of range for column "${column ?? "unknown"}" in "${tableRef}": ${pgMessage}${suffix}`
|
|
325
343
|
);
|
|
344
|
+
case "42703": // undefined_column
|
|
345
|
+
return new Error(
|
|
346
|
+
`Unknown column in "${tableRef}": ${pgMessage}. Check if your schema is up to date (run migrations).${suffix}`
|
|
347
|
+
);
|
|
348
|
+
case "42P01": // undefined_table
|
|
349
|
+
return new Error(
|
|
350
|
+
`Table not found for "${collectionSlug}": ${pgMessage}. Check if your schema is up to date (run migrations).${suffix}`
|
|
351
|
+
);
|
|
352
|
+
default: {
|
|
353
|
+
// Unhandled PG code — still surface the actual database message
|
|
354
|
+
const parts = [`Database error in "${collectionSlug}" [${pgError.code}]: ${pgMessage}`];
|
|
355
|
+
if (detail) parts.push(`Detail: ${detail}`);
|
|
356
|
+
if (column) parts.push(`Column: ${column}`);
|
|
357
|
+
if (dataType) parts.push(`Data type: ${dataType}`);
|
|
358
|
+
if (constraint) parts.push(`Constraint: ${constraint}`);
|
|
359
|
+
if (hint) parts.push(`Hint: ${hint}`);
|
|
360
|
+
return new Error(parts.join(". "));
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// No PG error found — try to extract a useful message from the
|
|
366
|
+
// Drizzle wrapper instead of leaking the raw SQL query + params.
|
|
367
|
+
const causeMessage = this.extractCauseMessage(error);
|
|
368
|
+
if (causeMessage) {
|
|
369
|
+
return new Error(`Database error in "${collectionSlug}": ${causeMessage}`);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Last resort: use the original error message but strip the SQL query
|
|
373
|
+
if (error instanceof Error) {
|
|
374
|
+
const cleaned = this.stripSqlFromMessage(error.message, collectionSlug);
|
|
375
|
+
return new Error(cleaned);
|
|
376
|
+
}
|
|
377
|
+
return new Error(`Database error in "${collectionSlug}": ${String(error)}`);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Walk the error cause chain and return the deepest meaningful message.
|
|
382
|
+
*/
|
|
383
|
+
private extractCauseMessage(error: unknown): string | null {
|
|
384
|
+
if (!error || typeof error !== "object") return null;
|
|
385
|
+
const err = error as Error & { cause?: unknown };
|
|
386
|
+
|
|
387
|
+
if (err.cause && typeof err.cause === "object") {
|
|
388
|
+
const deeper = this.extractCauseMessage(err.cause);
|
|
389
|
+
if (deeper) return deeper;
|
|
390
|
+
// The cause itself has a message
|
|
391
|
+
if (err.cause instanceof Error && err.cause.message) {
|
|
392
|
+
return err.cause.message;
|
|
326
393
|
}
|
|
327
394
|
}
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
328
397
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
398
|
+
/**
|
|
399
|
+
* Strip the raw SQL query from a Drizzle "Failed query: ..." message,
|
|
400
|
+
* keeping only the error description.
|
|
401
|
+
*/
|
|
402
|
+
private stripSqlFromMessage(message: string, collectionSlug: string): string {
|
|
403
|
+
// Drizzle format: "Failed query: <SQL>\nparams: <params>"
|
|
404
|
+
if (message.startsWith("Failed query:")) {
|
|
405
|
+
return `Failed to save entity in "${collectionSlug}". Check server logs for details.`;
|
|
406
|
+
}
|
|
407
|
+
return message;
|
|
332
408
|
}
|
|
333
409
|
|
|
334
410
|
/**
|
|
335
411
|
* Extract the underlying PostgreSQL error from a Drizzle wrapper.
|
|
336
412
|
* Drizzle wraps PG errors in a `cause` property.
|
|
337
413
|
*/
|
|
338
|
-
private extractPgError(error: unknown): (Error & { code?: string; detail?: unknown; constraint?: unknown; column?: unknown; table?: unknown }) | null {
|
|
414
|
+
private extractPgError(error: unknown): (Error & { code?: string; detail?: unknown; hint?: unknown; constraint?: unknown; column?: unknown; table?: unknown; dataType?: unknown }) | null {
|
|
339
415
|
if (!error || typeof error !== "object") return null;
|
|
340
416
|
|
|
341
417
|
const err = error as Error & { code?: string; cause?: unknown; detail?: unknown };
|
|
342
418
|
|
|
343
419
|
// Check if the error itself has a PG error code
|
|
344
|
-
if (err.code && /^[0-
|
|
345
|
-
return err as Error & { code: string; detail?: unknown; constraint?: unknown; column?: unknown; table?: unknown };
|
|
420
|
+
if (err.code && /^[0-9A-Z]{5}$/.test(err.code)) {
|
|
421
|
+
return err as Error & { code: string; detail?: unknown; hint?: unknown; constraint?: unknown; column?: unknown; table?: unknown; dataType?: unknown };
|
|
346
422
|
}
|
|
347
423
|
|
|
348
424
|
// Check the cause chain (Drizzle wraps PG errors)
|
|
@@ -1199,4 +1199,132 @@ describe("generateDrizzleSchema columnName support", () => {
|
|
|
1199
1199
|
expect(result).toContain('"service_provider_140a"');
|
|
1200
1200
|
expect(result).toContain('"insurance_id_140a"');
|
|
1201
1201
|
});
|
|
1202
|
+
|
|
1203
|
+
describe("generateDrizzleSchema autoValue date properties", () => {
|
|
1204
|
+
|
|
1205
|
+
it("should add .default(sql`now()`) for on_create autoValue", async () => {
|
|
1206
|
+
const collections: EntityCollection[] = [
|
|
1207
|
+
{
|
|
1208
|
+
slug: "articles",
|
|
1209
|
+
table: "articles",
|
|
1210
|
+
name: "Articles",
|
|
1211
|
+
properties: {
|
|
1212
|
+
title: { type: "string" },
|
|
1213
|
+
created_at: {
|
|
1214
|
+
type: "date",
|
|
1215
|
+
autoValue: "on_create"
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
];
|
|
1220
|
+
|
|
1221
|
+
const result = await generateSchema(collections, true);
|
|
1222
|
+
|
|
1223
|
+
// on_create should produce .default(sql`now()`)
|
|
1224
|
+
expect(result).toContain(".default(sql`now()`)");
|
|
1225
|
+
// No $onUpdate or triggers — on_update logic lives in the backend driver
|
|
1226
|
+
expect(result).not.toContain(".$onUpdate");
|
|
1227
|
+
expect(result).not.toContain("CREATE OR REPLACE TRIGGER");
|
|
1228
|
+
expect(result).not.toContain("CREATE OR REPLACE FUNCTION");
|
|
1229
|
+
});
|
|
1230
|
+
|
|
1231
|
+
it("should add .default(sql`now()`) for on_update autoValue (INSERT default only)", async () => {
|
|
1232
|
+
const collections: EntityCollection[] = [
|
|
1233
|
+
{
|
|
1234
|
+
slug: "articles",
|
|
1235
|
+
table: "articles",
|
|
1236
|
+
name: "Articles",
|
|
1237
|
+
properties: {
|
|
1238
|
+
title: { type: "string" },
|
|
1239
|
+
updated_at: {
|
|
1240
|
+
type: "date",
|
|
1241
|
+
autoValue: "on_update"
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
];
|
|
1246
|
+
|
|
1247
|
+
const result = await generateSchema(collections, true);
|
|
1248
|
+
|
|
1249
|
+
// on_update should produce .default(sql`now()`) for initial INSERT value
|
|
1250
|
+
expect(result).toContain(".default(sql`now()`)");
|
|
1251
|
+
// No $onUpdate or triggers — update logic is handled by the backend driver
|
|
1252
|
+
expect(result).not.toContain(".$onUpdate");
|
|
1253
|
+
expect(result).not.toContain("CREATE OR REPLACE TRIGGER");
|
|
1254
|
+
});
|
|
1255
|
+
|
|
1256
|
+
it("should not modify date columns without autoValue", async () => {
|
|
1257
|
+
const collections: EntityCollection[] = [
|
|
1258
|
+
{
|
|
1259
|
+
slug: "events",
|
|
1260
|
+
table: "events",
|
|
1261
|
+
name: "Events",
|
|
1262
|
+
properties: {
|
|
1263
|
+
name: { type: "string" },
|
|
1264
|
+
event_date: { type: "date" }
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
];
|
|
1268
|
+
|
|
1269
|
+
const result = await generateSchema(collections, true);
|
|
1270
|
+
|
|
1271
|
+
// A plain date should NOT have any autoValue-related modifiers
|
|
1272
|
+
expect(result).not.toContain(".default(sql`now()`)");
|
|
1273
|
+
expect(result).not.toContain(".$onUpdate");
|
|
1274
|
+
});
|
|
1275
|
+
|
|
1276
|
+
it("should handle both on_create and on_update in the same collection", async () => {
|
|
1277
|
+
const collections: EntityCollection[] = [
|
|
1278
|
+
{
|
|
1279
|
+
slug: "posts",
|
|
1280
|
+
table: "posts",
|
|
1281
|
+
name: "Posts",
|
|
1282
|
+
properties: {
|
|
1283
|
+
title: { type: "string" },
|
|
1284
|
+
created_at: {
|
|
1285
|
+
type: "date",
|
|
1286
|
+
autoValue: "on_create"
|
|
1287
|
+
},
|
|
1288
|
+
updated_at: {
|
|
1289
|
+
type: "date",
|
|
1290
|
+
autoValue: "on_update"
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
];
|
|
1295
|
+
|
|
1296
|
+
const result = await generateSchema(collections, true);
|
|
1297
|
+
|
|
1298
|
+
// Both should get .default(sql`now()`) for INSERT defaults
|
|
1299
|
+
expect(result).toMatch(/created_at:.*\.default\(sql`now\(\)`\)/);
|
|
1300
|
+
expect(result).toMatch(/updated_at:.*\.default\(sql`now\(\)`\)/);
|
|
1301
|
+
// No $onUpdate or triggers
|
|
1302
|
+
expect(result).not.toContain(".$onUpdate");
|
|
1303
|
+
expect(result).not.toContain("CREATE OR REPLACE TRIGGER");
|
|
1304
|
+
});
|
|
1305
|
+
|
|
1306
|
+
it("should handle on_create with date columnType", async () => {
|
|
1307
|
+
const collections: EntityCollection[] = [
|
|
1308
|
+
{
|
|
1309
|
+
slug: "logs",
|
|
1310
|
+
table: "logs",
|
|
1311
|
+
name: "Logs",
|
|
1312
|
+
properties: {
|
|
1313
|
+
message: { type: "string" },
|
|
1314
|
+
log_date: {
|
|
1315
|
+
type: "date",
|
|
1316
|
+
columnType: "date",
|
|
1317
|
+
autoValue: "on_create"
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
];
|
|
1322
|
+
|
|
1323
|
+
const result = await generateSchema(collections, true);
|
|
1324
|
+
|
|
1325
|
+
// Should use date() column with the default
|
|
1326
|
+
expect(result).toContain("date(\"log_date\"");
|
|
1327
|
+
expect(result).toContain(".default(sql`now()`)");
|
|
1328
|
+
});
|
|
1329
|
+
});
|
|
1202
1330
|
});
|
|
@@ -148,27 +148,27 @@ describe("generateCollectionFile", () => {
|
|
|
148
148
|
});
|
|
149
149
|
|
|
150
150
|
describe("enum support", () => {
|
|
151
|
-
it("generates
|
|
151
|
+
it("generates enum for USER-DEFINED columns with matching enum", () => {
|
|
152
152
|
const enumMap = new Map([["order_status", ["pending", "shipped", "delivered"]]]);
|
|
153
153
|
const meta = makeSimpleTable("orders", [
|
|
154
154
|
mkCol("orders", "id", { data_type: "uuid", udt_name: "uuid", is_nullable: "NO" }),
|
|
155
155
|
mkCol("orders", "status", { data_type: "USER-DEFINED", udt_name: "order_status" }),
|
|
156
156
|
]);
|
|
157
157
|
const result = generateCollectionFile("orders", meta, [], new Set(), new Map([["orders", meta]]), enumMap);
|
|
158
|
-
expect(result).toContain('
|
|
158
|
+
expect(result).toContain('enum:');
|
|
159
159
|
expect(result).toContain('{ id: "pending", label: "Pending" }');
|
|
160
160
|
expect(result).toContain('{ id: "shipped", label: "Shipped" }');
|
|
161
161
|
expect(result).toContain('{ id: "delivered", label: "Delivered" }');
|
|
162
162
|
expect(result).toContain('type: "string"');
|
|
163
163
|
});
|
|
164
164
|
|
|
165
|
-
it("does NOT add
|
|
165
|
+
it("does NOT add enum for USER-DEFINED without matching enum", () => {
|
|
166
166
|
const meta = makeSimpleTable("things", [
|
|
167
167
|
mkCol("things", "id", { data_type: "uuid", udt_name: "uuid", is_nullable: "NO" }),
|
|
168
168
|
mkCol("things", "geom", { data_type: "USER-DEFINED", udt_name: "geometry" }),
|
|
169
169
|
]);
|
|
170
170
|
const result = generateCollectionFile("things", meta, [], new Set(), new Map([["things", meta]]), new Map());
|
|
171
|
-
expect(result).not.toContain("
|
|
171
|
+
expect(result).not.toContain("enum");
|
|
172
172
|
});
|
|
173
173
|
|
|
174
174
|
it("humanizes enum value labels with underscores", () => {
|
|
@@ -264,7 +264,7 @@ describe("generateCollectionFile", () => {
|
|
|
264
264
|
]);
|
|
265
265
|
const result = generateCollectionFile("t", meta, [], new Set(), new Map([["t", meta]]), enumMap);
|
|
266
266
|
expect(result).not.toContain("storagePath");
|
|
267
|
-
expect(result).toContain("
|
|
267
|
+
expect(result).toContain("enum:");
|
|
268
268
|
});
|
|
269
269
|
});
|
|
270
270
|
|