@rebasepro/server-postgresql 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -89
- package/dist/{server-postgresql/src/PostgresAdapter.d.ts → PostgresAdapter.d.ts} +1 -1
- package/dist/{server-postgresql/src/PostgresBackendDriver.d.ts → PostgresBackendDriver.d.ts} +2 -2
- package/dist/{server-postgresql/src/PostgresBootstrapper.d.ts → PostgresBootstrapper.d.ts} +11 -1
- package/dist/{server-postgresql/src/auth → auth}/services.d.ts +11 -11
- package/dist/{server-postgresql/src/collections → collections}/PostgresCollectionRegistry.d.ts +4 -0
- package/dist/{server-postgresql/src/data-transformer.d.ts → data-transformer.d.ts} +0 -3
- package/dist/{server-postgresql/src/databasePoolManager.d.ts → databasePoolManager.d.ts} +1 -1
- package/dist/index.es.js +10174 -11184
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +10735 -11462
- package/dist/index.umd.js.map +1 -1
- package/dist/{server-postgresql/src/services → services}/EntityPersistService.d.ts +0 -14
- package/dist/types.d.ts +3 -0
- package/dist/utils/pg-error-utils.d.ts +55 -0
- package/dist/{server-postgresql/src/websocket.d.ts → websocket.d.ts} +8 -3
- package/package.json +24 -21
- package/src/PostgresAdapter.ts +9 -10
- package/src/PostgresBackendDriver.ts +135 -122
- package/src/PostgresBootstrapper.ts +90 -16
- package/src/auth/ensure-tables.ts +28 -5
- package/src/auth/services.ts +56 -45
- package/src/cli.ts +140 -110
- package/src/collections/PostgresCollectionRegistry.ts +7 -0
- package/src/connection.ts +11 -6
- package/src/data-transformer.ts +73 -109
- package/src/databasePoolManager.ts +5 -3
- package/src/history/HistoryService.ts +3 -2
- package/src/history/ensure-history-table.ts +5 -4
- package/src/schema/auth-schema.ts +1 -2
- package/src/schema/doctor-cli.ts +2 -1
- package/src/schema/doctor.ts +40 -37
- package/src/schema/generate-drizzle-schema-logic.ts +56 -18
- package/src/schema/generate-drizzle-schema.ts +11 -11
- package/src/schema/introspect-db-inference.ts +25 -25
- package/src/schema/introspect-db-logic.ts +38 -38
- package/src/schema/introspect-db.ts +28 -27
- package/src/services/BranchService.ts +14 -0
- package/src/services/EntityFetchService.ts +28 -25
- package/src/services/EntityPersistService.ts +11 -124
- package/src/services/RelationService.ts +57 -37
- package/src/services/entity-helpers.ts +6 -2
- package/src/services/realtimeService.ts +45 -32
- package/src/types.ts +4 -0
- package/src/utils/drizzle-conditions.ts +31 -15
- package/src/utils/pg-error-utils.ts +211 -0
- package/src/websocket.ts +51 -33
- package/test/auth-services.test.ts +36 -19
- package/test/batch-many-to-many-regression.test.ts +119 -39
- package/test/data-transformer-hardening.test.ts +67 -33
- package/test/data-transformer.test.ts +4 -2
- package/test/doctor.test.ts +10 -5
- package/test/drizzle-conditions.test.ts +59 -6
- package/test/generate-drizzle-schema.test.ts +65 -40
- package/test/introspect-db-generation.test.ts +179 -81
- package/test/introspect-db-utils.test.ts +92 -37
- package/test/mocks/chalk.cjs +7 -0
- package/test/pg-error-utils.test.ts +221 -0
- package/test/postgresDataDriver.test.ts +14 -5
- package/test/property-ordering.test.ts +126 -79
- package/test/realtimeService.test.ts +6 -2
- package/test/relation-pipeline-gaps.test.ts +84 -36
- package/test/relations.test.ts +247 -0
- package/test/unmapped-tables-safety.test.ts +14 -6
- package/test/websocket.test.ts +1 -1
- package/tsconfig.json +5 -0
- package/tsconfig.prod.json +3 -0
- package/vite.config.ts +5 -5
- package/dist/common/src/collections/CollectionRegistry.d.ts +0 -56
- package/dist/common/src/collections/default-collections.d.ts +0 -9
- package/dist/common/src/collections/index.d.ts +0 -2
- package/dist/common/src/data/buildRebaseData.d.ts +0 -14
- package/dist/common/src/data/query_builder.d.ts +0 -55
- package/dist/common/src/index.d.ts +0 -4
- package/dist/common/src/util/builders.d.ts +0 -57
- package/dist/common/src/util/callbacks.d.ts +0 -6
- package/dist/common/src/util/collections.d.ts +0 -11
- package/dist/common/src/util/common.d.ts +0 -2
- package/dist/common/src/util/conditions.d.ts +0 -26
- package/dist/common/src/util/entities.d.ts +0 -58
- package/dist/common/src/util/enums.d.ts +0 -3
- package/dist/common/src/util/index.d.ts +0 -16
- package/dist/common/src/util/navigation_from_path.d.ts +0 -34
- package/dist/common/src/util/navigation_utils.d.ts +0 -20
- package/dist/common/src/util/parent_references_from_path.d.ts +0 -6
- package/dist/common/src/util/paths.d.ts +0 -14
- package/dist/common/src/util/permissions.d.ts +0 -6
- package/dist/common/src/util/references.d.ts +0 -2
- package/dist/common/src/util/relations.d.ts +0 -22
- package/dist/common/src/util/resolutions.d.ts +0 -72
- package/dist/common/src/util/storage.d.ts +0 -24
- package/dist/types/src/controllers/analytics_controller.d.ts +0 -7
- package/dist/types/src/controllers/auth.d.ts +0 -104
- package/dist/types/src/controllers/client.d.ts +0 -168
- package/dist/types/src/controllers/collection_registry.d.ts +0 -46
- package/dist/types/src/controllers/customization_controller.d.ts +0 -60
- package/dist/types/src/controllers/data.d.ts +0 -207
- package/dist/types/src/controllers/data_driver.d.ts +0 -218
- package/dist/types/src/controllers/database_admin.d.ts +0 -11
- package/dist/types/src/controllers/dialogs_controller.d.ts +0 -36
- package/dist/types/src/controllers/effective_role.d.ts +0 -4
- package/dist/types/src/controllers/email.d.ts +0 -36
- package/dist/types/src/controllers/index.d.ts +0 -18
- package/dist/types/src/controllers/local_config_persistence.d.ts +0 -20
- package/dist/types/src/controllers/navigation.d.ts +0 -225
- package/dist/types/src/controllers/registry.d.ts +0 -63
- package/dist/types/src/controllers/side_dialogs_controller.d.ts +0 -67
- package/dist/types/src/controllers/side_entity_controller.d.ts +0 -97
- package/dist/types/src/controllers/snackbar.d.ts +0 -24
- package/dist/types/src/controllers/storage.d.ts +0 -171
- package/dist/types/src/index.d.ts +0 -4
- package/dist/types/src/rebase_context.d.ts +0 -122
- package/dist/types/src/types/auth_adapter.d.ts +0 -301
- package/dist/types/src/types/backend.d.ts +0 -536
- package/dist/types/src/types/backend_hooks.d.ts +0 -172
- package/dist/types/src/types/builders.d.ts +0 -15
- package/dist/types/src/types/chips.d.ts +0 -5
- package/dist/types/src/types/collections.d.ts +0 -941
- package/dist/types/src/types/component_ref.d.ts +0 -47
- package/dist/types/src/types/cron.d.ts +0 -102
- package/dist/types/src/types/data_source.d.ts +0 -64
- package/dist/types/src/types/database_adapter.d.ts +0 -94
- package/dist/types/src/types/entities.d.ts +0 -145
- package/dist/types/src/types/entity_actions.d.ts +0 -104
- package/dist/types/src/types/entity_callbacks.d.ts +0 -173
- package/dist/types/src/types/entity_link_builder.d.ts +0 -7
- package/dist/types/src/types/entity_overrides.d.ts +0 -10
- package/dist/types/src/types/entity_views.d.ts +0 -87
- package/dist/types/src/types/export_import.d.ts +0 -21
- package/dist/types/src/types/formex.d.ts +0 -40
- package/dist/types/src/types/index.d.ts +0 -28
- package/dist/types/src/types/locales.d.ts +0 -4
- package/dist/types/src/types/modify_collections.d.ts +0 -5
- package/dist/types/src/types/plugins.d.ts +0 -282
- package/dist/types/src/types/properties.d.ts +0 -1181
- package/dist/types/src/types/property_config.d.ts +0 -74
- package/dist/types/src/types/relations.d.ts +0 -336
- package/dist/types/src/types/slots.d.ts +0 -262
- package/dist/types/src/types/translations.d.ts +0 -900
- package/dist/types/src/types/user_management_delegate.d.ts +0 -86
- package/dist/types/src/types/websockets.d.ts +0 -78
- package/dist/types/src/users/index.d.ts +0 -1
- package/dist/types/src/users/user.d.ts +0 -50
- package/drizzle.test.config.ts +0 -10
- /package/dist/{server-postgresql/src/auth → auth}/ensure-tables.d.ts +0 -0
- /package/dist/{server-postgresql/src/cli.d.ts → cli.d.ts} +0 -0
- /package/dist/{server-postgresql/src/connection.d.ts → connection.d.ts} +0 -0
- /package/dist/{server-postgresql/src/history → history}/HistoryService.d.ts +0 -0
- /package/dist/{server-postgresql/src/history → history}/ensure-history-table.d.ts +0 -0
- /package/dist/{server-postgresql/src/index.d.ts → index.d.ts} +0 -0
- /package/dist/{server-postgresql/src/interfaces.d.ts → interfaces.d.ts} +0 -0
- /package/dist/{server-postgresql/src/schema → schema}/auth-schema.d.ts +0 -0
- /package/dist/{server-postgresql/src/schema → schema}/doctor-cli.d.ts +0 -0
- /package/dist/{server-postgresql/src/schema → schema}/doctor.d.ts +0 -0
- /package/dist/{server-postgresql/src/schema → schema}/generate-drizzle-schema-logic.d.ts +0 -0
- /package/dist/{server-postgresql/src/schema → schema}/generate-drizzle-schema.d.ts +0 -0
- /package/dist/{server-postgresql/src/schema → schema}/introspect-db-inference.d.ts +0 -0
- /package/dist/{server-postgresql/src/schema → schema}/introspect-db-logic.d.ts +0 -0
- /package/dist/{server-postgresql/src/schema → schema}/introspect-db.d.ts +0 -0
- /package/dist/{server-postgresql/src/schema → schema}/test-schema.d.ts +0 -0
- /package/dist/{server-postgresql/src/services → services}/BranchService.d.ts +0 -0
- /package/dist/{server-postgresql/src/services → services}/EntityFetchService.d.ts +0 -0
- /package/dist/{server-postgresql/src/services → services}/RelationService.d.ts +0 -0
- /package/dist/{server-postgresql/src/services → services}/entity-helpers.d.ts +0 -0
- /package/dist/{server-postgresql/src/services → services}/entityService.d.ts +0 -0
- /package/dist/{server-postgresql/src/services → services}/index.d.ts +0 -0
- /package/dist/{server-postgresql/src/services → services}/realtimeService.d.ts +0 -0
- /package/dist/{server-postgresql/src/utils → utils}/drizzle-conditions.d.ts +0 -0
|
@@ -18,8 +18,9 @@ import {
|
|
|
18
18
|
generateCollectionFile,
|
|
19
19
|
generateIndexContent,
|
|
20
20
|
mergeIndexContent,
|
|
21
|
-
safeHostFromUrl
|
|
21
|
+
safeHostFromUrl
|
|
22
22
|
} from "./introspect-db-logic";
|
|
23
|
+
import { logger } from "@rebasepro/server-core";
|
|
23
24
|
|
|
24
25
|
async function main() {
|
|
25
26
|
const args = arg(
|
|
@@ -31,15 +32,15 @@ async function main() {
|
|
|
31
32
|
"--data-inference": Boolean,
|
|
32
33
|
"-o": "--output",
|
|
33
34
|
"-c": "--collections",
|
|
34
|
-
"-f": "--force"
|
|
35
|
+
"-f": "--force"
|
|
35
36
|
},
|
|
36
37
|
{ permissive: true }
|
|
37
38
|
);
|
|
38
39
|
|
|
39
40
|
const cwd = process.cwd();
|
|
40
41
|
const isBackendDir = path.basename(cwd) === "backend";
|
|
41
|
-
const defaultOutDir = isBackendDir
|
|
42
|
-
? path.resolve(cwd, "..", "config", "collections")
|
|
42
|
+
const defaultOutDir = isBackendDir
|
|
43
|
+
? path.resolve(cwd, "..", "config", "collections")
|
|
43
44
|
: path.resolve(cwd, "config", "collections");
|
|
44
45
|
|
|
45
46
|
const outDir = args["--output"] || args["--collections"] || defaultOutDir;
|
|
@@ -67,7 +68,7 @@ async function main() {
|
|
|
67
68
|
|
|
68
69
|
const databaseUrl = process.env.DATABASE_URL || process.env.ADMIN_CONNECTION_STRING;
|
|
69
70
|
if (!databaseUrl) {
|
|
70
|
-
|
|
71
|
+
logger.error(chalk.red("✗ DATABASE_URL is not set. Make sure your .env file is configured."));
|
|
71
72
|
process.exit(1);
|
|
72
73
|
}
|
|
73
74
|
|
|
@@ -76,15 +77,15 @@ async function main() {
|
|
|
76
77
|
try {
|
|
77
78
|
await client.connect();
|
|
78
79
|
} catch (err) {
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
logger.error(chalk.red(`✗ Failed to connect to database: ${err instanceof Error ? err.message : String(err)}`));
|
|
81
|
+
logger.error(chalk.gray(" Check your DATABASE_URL and ensure the database is reachable."));
|
|
81
82
|
process.exit(1);
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
// Log the host portion safely — handle URLs without "@"
|
|
85
86
|
const hostPart = safeHostFromUrl(databaseUrl);
|
|
86
|
-
|
|
87
|
-
|
|
87
|
+
logger.info(chalk.gray(`Connected to database: ${hostPart}`));
|
|
88
|
+
logger.info(chalk.gray(`Introspecting schema '${pgSchema}'...`));
|
|
88
89
|
|
|
89
90
|
try {
|
|
90
91
|
// 1. Get Tables
|
|
@@ -162,7 +163,7 @@ async function main() {
|
|
|
162
163
|
const tablesMap = buildTablesMap(tables, columns, pks, fks);
|
|
163
164
|
const joinTables = identifyJoinTables(tablesMap);
|
|
164
165
|
|
|
165
|
-
|
|
166
|
+
logger.info(chalk.blue(`Found ${tablesMap.size} tables (including ${joinTables.size} detected join tables).`));
|
|
166
167
|
|
|
167
168
|
let runDataInference = false;
|
|
168
169
|
if (args["--data-inference"] !== undefined) {
|
|
@@ -173,12 +174,12 @@ async function main() {
|
|
|
173
174
|
output: process.stdout
|
|
174
175
|
});
|
|
175
176
|
const answer = await new Promise<string>((resolve) => rl.question(chalk.yellow("? Do you want to run comprehensive data inference on sampled rows to auto-detect types, formats, constraints, and UI configurations? (y/N) "), resolve));
|
|
176
|
-
runDataInference = answer.trim().toLowerCase() ===
|
|
177
|
+
runDataInference = answer.trim().toLowerCase() === "y";
|
|
177
178
|
rl.close();
|
|
178
179
|
}
|
|
179
180
|
|
|
180
181
|
if (runDataInference) {
|
|
181
|
-
|
|
182
|
+
logger.info(chalk.gray("Sampling database rows for data inference..."));
|
|
182
183
|
}
|
|
183
184
|
|
|
184
185
|
// Generate Collections
|
|
@@ -186,11 +187,11 @@ async function main() {
|
|
|
186
187
|
const skippedFiles: string[] = [];
|
|
187
188
|
|
|
188
189
|
const tablesToProcess = Array.from(tablesMap.entries()).filter(([tableName]) => !joinTables.has(tableName));
|
|
189
|
-
|
|
190
|
+
|
|
190
191
|
const BATCH_SIZE = 10;
|
|
191
192
|
for (let i = 0; i < tablesToProcess.length; i += BATCH_SIZE) {
|
|
192
193
|
const batch = tablesToProcess.slice(i, i + BATCH_SIZE);
|
|
193
|
-
|
|
194
|
+
|
|
194
195
|
await Promise.all(batch.map(async ([tableName, meta]) => {
|
|
195
196
|
// ── File overwrite protection ──────────────────────────────
|
|
196
197
|
const filePath = path.join(outDir, `${tableName}.ts`);
|
|
@@ -205,7 +206,7 @@ async function main() {
|
|
|
205
206
|
const { rows } = await client.query(`SELECT * FROM "${pgSchema}"."${tableName}" LIMIT 100`);
|
|
206
207
|
sampleData = rows;
|
|
207
208
|
} catch (err) {
|
|
208
|
-
|
|
209
|
+
logger.error(chalk.yellow(`⚠ Failed to sample data for table ${tableName}: ${err instanceof Error ? err.message : String(err)}`));
|
|
209
210
|
}
|
|
210
211
|
}
|
|
211
212
|
|
|
@@ -216,12 +217,12 @@ async function main() {
|
|
|
216
217
|
joinTables,
|
|
217
218
|
tablesMap,
|
|
218
219
|
enumMap,
|
|
219
|
-
sampleData
|
|
220
|
+
sampleData
|
|
220
221
|
);
|
|
221
222
|
|
|
222
223
|
fs.writeFileSync(filePath, fileContent, "utf-8");
|
|
223
224
|
generatedFiles.push(tableName);
|
|
224
|
-
|
|
225
|
+
logger.info(chalk.green(` ✓ ${filePath}`));
|
|
225
226
|
}));
|
|
226
227
|
}
|
|
227
228
|
|
|
@@ -238,21 +239,21 @@ async function main() {
|
|
|
238
239
|
const indexContent = generateIndexContent(generatedFiles);
|
|
239
240
|
fs.writeFileSync(indexPath, indexContent, "utf-8");
|
|
240
241
|
}
|
|
241
|
-
|
|
242
|
+
logger.info(chalk.green(` ✓ ${indexPath}`));
|
|
242
243
|
}
|
|
243
244
|
|
|
244
|
-
|
|
245
|
+
logger.info("");
|
|
245
246
|
if (skippedFiles.length > 0) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
247
|
+
logger.info(chalk.yellow(`⚠ Skipped ${skippedFiles.length} existing file(s): ${skippedFiles.join(", ")}`));
|
|
248
|
+
logger.info(chalk.gray(" Use --force to overwrite existing files."));
|
|
249
|
+
logger.info("");
|
|
249
250
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
251
|
+
logger.info(chalk.bold.green(`✓ Introspected ${tablesMap.size} tables — generated ${generatedFiles.length} collection(s).`));
|
|
252
|
+
logger.info(chalk.gray(` Review the generated files in ${outDir} and customize properties as needed.`));
|
|
253
|
+
logger.info("");
|
|
253
254
|
|
|
254
255
|
} catch (e) {
|
|
255
|
-
|
|
256
|
+
logger.error(chalk.red(`✗ Error introspecting database: ${e instanceof Error ? e.message : String(e)}`));
|
|
256
257
|
process.exit(1);
|
|
257
258
|
} finally {
|
|
258
259
|
await client.end();
|
|
@@ -260,6 +261,6 @@ async function main() {
|
|
|
260
261
|
}
|
|
261
262
|
|
|
262
263
|
main().catch((err) => {
|
|
263
|
-
|
|
264
|
+
logger.error(String(err));
|
|
264
265
|
process.exit(1);
|
|
265
266
|
});
|
|
@@ -18,6 +18,16 @@ const BRANCH_DB_PREFIX = "rb_";
|
|
|
18
18
|
/** Fully-qualified metadata table in the rebase schema. */
|
|
19
19
|
const BRANCHES_TABLE = "rebase.branches";
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Validate that a user-provided identifier only contains safe characters.
|
|
23
|
+
* Throws if the value contains characters outside [a-zA-Z0-9_-].
|
|
24
|
+
*/
|
|
25
|
+
function validateIdentifier(value: string, label: string): void {
|
|
26
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(value)) {
|
|
27
|
+
throw new Error(`Invalid ${label}: only letters, digits, underscores, and hyphens are allowed.`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
21
31
|
/**
|
|
22
32
|
* Sanitize a user-provided branch name to a safe PostgreSQL identifier.
|
|
23
33
|
* Only allows alphanumeric characters and underscores.
|
|
@@ -70,6 +80,10 @@ export class BranchService {
|
|
|
70
80
|
* @param options.source Source database to clone; defaults to the main database.
|
|
71
81
|
*/
|
|
72
82
|
async createBranch(name: string, options?: { source?: string }): Promise<BranchInfo> {
|
|
83
|
+
if (options?.source) {
|
|
84
|
+
validateIdentifier(options.source, "source database name");
|
|
85
|
+
}
|
|
86
|
+
|
|
73
87
|
const dbName = toBranchDbName(name);
|
|
74
88
|
const sanitizedName = sanitizeBranchName(name);
|
|
75
89
|
const sourceDb = options?.source || this.poolManager.defaultDatabaseName;
|
|
@@ -16,6 +16,7 @@ import { RelationService } from "./RelationService";
|
|
|
16
16
|
import { RelationalQueryBuilder } from "drizzle-orm/pg-core/query-builders/query";
|
|
17
17
|
import { DrizzleClient } from "../interfaces";
|
|
18
18
|
import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
|
|
19
|
+
import { logger } from "@rebasepro/server-core";
|
|
19
20
|
|
|
20
21
|
/** Type-safe accessor for Drizzle's relational query API via dynamic table name */
|
|
21
22
|
type DbQueryAccessor = Record<string, RelationalQueryBuilder<any, any>> | undefined;
|
|
@@ -269,7 +270,7 @@ export class EntityFetchService {
|
|
|
269
270
|
);
|
|
270
271
|
}
|
|
271
272
|
} catch (e) {
|
|
272
|
-
|
|
273
|
+
logger.warn(`Could not resolve joinPath relation '${key}'`, { error: e });
|
|
273
274
|
}
|
|
274
275
|
});
|
|
275
276
|
|
|
@@ -322,7 +323,7 @@ export class EntityFetchService {
|
|
|
322
323
|
}
|
|
323
324
|
}
|
|
324
325
|
} catch (e) {
|
|
325
|
-
|
|
326
|
+
logger.warn(`Could not batch resolve joinPath relation '${key}'`, { error: e });
|
|
326
327
|
}
|
|
327
328
|
}
|
|
328
329
|
}
|
|
@@ -400,7 +401,7 @@ export class EntityFetchService {
|
|
|
400
401
|
}
|
|
401
402
|
}
|
|
402
403
|
} catch (e) {
|
|
403
|
-
|
|
404
|
+
logger.warn(`Could not batch resolve joinPath relation '${key}' for REST`, { error: e });
|
|
404
405
|
}
|
|
405
406
|
}
|
|
406
407
|
}
|
|
@@ -625,10 +626,10 @@ export class EntityFetchService {
|
|
|
625
626
|
return entity;
|
|
626
627
|
} catch (e) {
|
|
627
628
|
if (e instanceof Error && e.message.includes("not enough information to infer relation")) {
|
|
628
|
-
|
|
629
|
-
|
|
629
|
+
logger.error(`[EntityFetchService] Relation inference error for collection '${collectionPath}': ${e.message}`);
|
|
630
|
+
logger.error("Hint: This usually means a relation in your drizzle schema is missing a reciprocal 'one()' or 'many()' definition. Run 'rebase schema generate' to fix this.");
|
|
630
631
|
}
|
|
631
|
-
|
|
632
|
+
logger.warn(`[EntityFetchService] db.query.findFirst failed for ${collectionPath}, falling back to db.select`, { error: e });
|
|
632
633
|
}
|
|
633
634
|
}
|
|
634
635
|
|
|
@@ -675,7 +676,7 @@ export class EntityFetchService {
|
|
|
675
676
|
(values as Record<string, unknown>)[key] = createRelationRef(e.id, e.path);
|
|
676
677
|
}
|
|
677
678
|
} catch (e) {
|
|
678
|
-
|
|
679
|
+
logger.warn(`Could not resolve one-to-one relation property: ${key}`, { error: e });
|
|
679
680
|
}
|
|
680
681
|
}
|
|
681
682
|
}
|
|
@@ -749,10 +750,10 @@ export class EntityFetchService {
|
|
|
749
750
|
return entities;
|
|
750
751
|
} catch (e) {
|
|
751
752
|
if (e instanceof Error && e.message.includes("not enough information to infer relation")) {
|
|
752
|
-
|
|
753
|
-
|
|
753
|
+
logger.error(`[EntityFetchService] Relation inference error for collection '${collectionPath}': ${e.message}`);
|
|
754
|
+
logger.error("Hint: This usually means a relation in your drizzle schema is missing a reciprocal 'one()' or 'many()' definition. Run 'rebase schema generate' to fix this.");
|
|
754
755
|
}
|
|
755
|
-
|
|
756
|
+
logger.warn(`[EntityFetchService] db.query.findMany failed for ${collectionPath}, falling back to db.select`, { error: e });
|
|
756
757
|
}
|
|
757
758
|
}
|
|
758
759
|
|
|
@@ -764,7 +765,8 @@ export class EntityFetchService {
|
|
|
764
765
|
}
|
|
765
766
|
|
|
766
767
|
let query = vectorMeta
|
|
767
|
-
? this.db.select({ table_row: table,
|
|
768
|
+
? this.db.select({ table_row: table,
|
|
769
|
+
_distance: vectorMeta.distanceSelect }).from(table).$dynamic()
|
|
768
770
|
: this.db.select().from(table).$dynamic();
|
|
769
771
|
const allConditions: SQL[] = [];
|
|
770
772
|
|
|
@@ -908,7 +910,7 @@ export class EntityFetchService {
|
|
|
908
910
|
}
|
|
909
911
|
});
|
|
910
912
|
} catch (e) {
|
|
911
|
-
|
|
913
|
+
logger.warn(`Could not batch load one-to-one relation property: ${key}`, { error: e });
|
|
912
914
|
}
|
|
913
915
|
}
|
|
914
916
|
|
|
@@ -935,7 +937,7 @@ export class EntityFetchService {
|
|
|
935
937
|
);
|
|
936
938
|
});
|
|
937
939
|
} catch (e) {
|
|
938
|
-
|
|
940
|
+
logger.warn(`Could not batch load many relation property: ${key}`, { error: e });
|
|
939
941
|
}
|
|
940
942
|
}
|
|
941
943
|
}
|
|
@@ -1250,10 +1252,10 @@ export class EntityFetchService {
|
|
|
1250
1252
|
return restRows;
|
|
1251
1253
|
} catch (e) {
|
|
1252
1254
|
if (e instanceof Error && e.message.includes("not enough information to infer relation")) {
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
+
logger.error(`[EntityFetchService] Relation inference error for collection '${collectionPath}': ${e.message}`);
|
|
1256
|
+
logger.error("Hint: This usually means a relation in your drizzle schema is missing a reciprocal 'one()' or 'many()' definition. Run 'rebase schema generate' to fix this.");
|
|
1255
1257
|
}
|
|
1256
|
-
|
|
1258
|
+
logger.warn(`[fetchCollectionForRest] db.query.findMany failed for ${collectionPath}, falling back`, { error: e });
|
|
1257
1259
|
}
|
|
1258
1260
|
}
|
|
1259
1261
|
|
|
@@ -1290,7 +1292,7 @@ export class EntityFetchService {
|
|
|
1290
1292
|
}
|
|
1291
1293
|
}
|
|
1292
1294
|
} catch (e) {
|
|
1293
|
-
|
|
1295
|
+
logger.warn(`[include] Failed to batch load one-to-one '${key}'`, { error: e });
|
|
1294
1296
|
}
|
|
1295
1297
|
}
|
|
1296
1298
|
|
|
@@ -1309,7 +1311,7 @@ export class EntityFetchService {
|
|
|
1309
1311
|
}));
|
|
1310
1312
|
}
|
|
1311
1313
|
} catch (e) {
|
|
1312
|
-
|
|
1314
|
+
logger.warn(`[include] Failed to batch load many '${key}'`, { error: e });
|
|
1313
1315
|
}
|
|
1314
1316
|
}
|
|
1315
1317
|
|
|
@@ -1364,10 +1366,10 @@ export class EntityFetchService {
|
|
|
1364
1366
|
return restRow;
|
|
1365
1367
|
} catch (e) {
|
|
1366
1368
|
if (e instanceof Error && e.message.includes("not enough information to infer relation")) {
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
+
logger.error(`[EntityFetchService] Relation inference error for collection '${collectionPath}': ${e.message}`);
|
|
1370
|
+
logger.error("Hint: This usually means a relation in your drizzle schema is missing a reciprocal 'one()' or 'many()' definition. Run 'rebase schema generate' to fix this.");
|
|
1369
1371
|
}
|
|
1370
|
-
|
|
1372
|
+
logger.warn(`[fetchEntityForRest] db.query.findFirst failed for ${collectionPath}, falling back`, { error: e });
|
|
1371
1373
|
}
|
|
1372
1374
|
}
|
|
1373
1375
|
|
|
@@ -1415,7 +1417,7 @@ export class EntityFetchService {
|
|
|
1415
1417
|
}));
|
|
1416
1418
|
}
|
|
1417
1419
|
} catch (e) {
|
|
1418
|
-
|
|
1420
|
+
logger.warn(`[include] Failed to load relation '${key}'`, { error: e });
|
|
1419
1421
|
}
|
|
1420
1422
|
}
|
|
1421
1423
|
|
|
@@ -1450,7 +1452,8 @@ export class EntityFetchService {
|
|
|
1450
1452
|
}
|
|
1451
1453
|
|
|
1452
1454
|
let query = vectorMeta
|
|
1453
|
-
? this.db.select({ table_row: table,
|
|
1455
|
+
? this.db.select({ table_row: table,
|
|
1456
|
+
_distance: vectorMeta.distanceSelect }).from(table).$dynamic()
|
|
1454
1457
|
: this.db.select().from(table).$dynamic();
|
|
1455
1458
|
const allConditions: SQL[] = [];
|
|
1456
1459
|
|
|
@@ -1621,7 +1624,7 @@ export class EntityFetchService {
|
|
|
1621
1624
|
return flat;
|
|
1622
1625
|
});
|
|
1623
1626
|
} catch (e) {
|
|
1624
|
-
|
|
1627
|
+
logger.warn(`[include] Drizzle relational query failed for '${collectionPath}', falling back`, { error: e });
|
|
1625
1628
|
return null;
|
|
1626
1629
|
}
|
|
1627
1630
|
}
|
|
@@ -1647,7 +1650,7 @@ export class EntityFetchService {
|
|
|
1647
1650
|
const relation = resolvedRelations[relationKey];
|
|
1648
1651
|
|
|
1649
1652
|
if (!relation) {
|
|
1650
|
-
|
|
1653
|
+
logger.warn(`[batchFetchManyRelatedEntities] Relation '${relationKey}' not found, skipping`);
|
|
1651
1654
|
return new Map();
|
|
1652
1655
|
}
|
|
1653
1656
|
|
|
@@ -16,6 +16,8 @@ import { RelationService } from "./RelationService";
|
|
|
16
16
|
import { EntityFetchService } from "./EntityFetchService";
|
|
17
17
|
import { DrizzleClient } from "../interfaces";
|
|
18
18
|
import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
|
|
19
|
+
import { logger } from "@rebasepro/server-core";
|
|
20
|
+
import { extractPgError, extractCauseMessage, pgErrorToFriendlyMessage } from "../utils/pg-error-utils";
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* Service for handling all entity write operations.
|
|
@@ -128,7 +130,7 @@ export class EntityPersistService {
|
|
|
128
130
|
const targetColumnNames = DrizzleConditionBuilder.getColumnNamesFromColumns(relevantJoinStep.on.to);
|
|
129
131
|
targetColumnName = targetColumnNames[0];
|
|
130
132
|
} else {
|
|
131
|
-
|
|
133
|
+
logger.warn(`Could not find specific join step for target table ${targetTableName} in relation '${relationKey}'.`);
|
|
132
134
|
const targetColumnNames = DrizzleConditionBuilder.getColumnNamesFromColumns(relation.joinPath[0].on.to);
|
|
133
135
|
targetColumnName = targetColumnNames[0];
|
|
134
136
|
}
|
|
@@ -149,7 +151,7 @@ export class EntityPersistService {
|
|
|
149
151
|
|
|
150
152
|
const existingValue = (effectiveValues as Record<string, unknown>)[targetColumnName];
|
|
151
153
|
if (existingValue !== undefined && existingValue !== null && existingValue !== parsedParentId) {
|
|
152
|
-
|
|
154
|
+
logger.warn(`Overriding provided value '${existingValue}' for FK '${targetColumnName}' with path parent id '${parsedParentId}'.`);
|
|
153
155
|
}
|
|
154
156
|
(effectiveValues as Record<string, unknown>)[targetColumnName] = parsedParentId;
|
|
155
157
|
break;
|
|
@@ -302,139 +304,24 @@ export class EntityPersistService {
|
|
|
302
304
|
* Translate raw PostgreSQL / Drizzle errors into user-friendly messages.
|
|
303
305
|
*/
|
|
304
306
|
private toUserFriendlyError(error: unknown, collectionSlug: string): Error {
|
|
305
|
-
|
|
306
|
-
const pgError = this.extractPgError(error);
|
|
307
|
+
const pgError = extractPgError(error);
|
|
307
308
|
|
|
308
309
|
if (pgError) {
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
const constraint = pgError.constraint as string | undefined;
|
|
312
|
-
const column = pgError.column as string | undefined;
|
|
313
|
-
const table = pgError.table as string | undefined;
|
|
314
|
-
const dataType = pgError.dataType as string | undefined;
|
|
315
|
-
const pgMessage = pgError.message || "Unknown database error";
|
|
316
|
-
|
|
317
|
-
const suffix = hint ? ` Hint: ${hint}` : "";
|
|
318
|
-
const tableRef = table ?? collectionSlug;
|
|
319
|
-
|
|
320
|
-
switch (pgError.code) {
|
|
321
|
-
case "23503": // foreign_key_violation
|
|
322
|
-
return new Error(
|
|
323
|
-
detail
|
|
324
|
-
? `Foreign key constraint violated: ${detail}${suffix}`
|
|
325
|
-
: `Cannot save: a foreign key constraint${constraint ? ` (${constraint})` : ""} was violated in "${collectionSlug}".${suffix}`
|
|
326
|
-
);
|
|
327
|
-
case "23505": // unique_violation
|
|
328
|
-
return new Error(
|
|
329
|
-
detail
|
|
330
|
-
? `Duplicate value: ${detail}${suffix}`
|
|
331
|
-
: `Cannot save: a unique constraint${constraint ? ` (${constraint})` : ""} was violated in "${collectionSlug}".${suffix}`
|
|
332
|
-
);
|
|
333
|
-
case "23502": // not_null_violation
|
|
334
|
-
return new Error(
|
|
335
|
-
`Missing required field: "${column ?? "unknown"}" in "${tableRef}" cannot be empty.${suffix}`
|
|
336
|
-
);
|
|
337
|
-
case "23514": // check_violation
|
|
338
|
-
return new Error(
|
|
339
|
-
`Validation failed: a check constraint${constraint ? ` (${constraint})` : ""} was violated in "${collectionSlug}".${suffix}`
|
|
340
|
-
);
|
|
341
|
-
case "22P02": // invalid_text_representation (e.g. invalid UUID, wrong enum value)
|
|
342
|
-
return new Error(
|
|
343
|
-
`Invalid data format in "${collectionSlug}": ${pgMessage}${suffix}`
|
|
344
|
-
);
|
|
345
|
-
case "22001": // string_data_right_truncation (value too long)
|
|
346
|
-
return new Error(
|
|
347
|
-
`Value too long for column "${column ?? "unknown"}" in "${tableRef}": ${pgMessage}${suffix}`
|
|
348
|
-
);
|
|
349
|
-
case "22003": // numeric_value_out_of_range
|
|
350
|
-
return new Error(
|
|
351
|
-
`Numeric value out of range for column "${column ?? "unknown"}" in "${tableRef}": ${pgMessage}${suffix}`
|
|
352
|
-
);
|
|
353
|
-
case "42703": // undefined_column
|
|
354
|
-
return new Error(
|
|
355
|
-
`Unknown column in "${tableRef}": ${pgMessage}. Check if your schema is up to date (run migrations).${suffix}`
|
|
356
|
-
);
|
|
357
|
-
case "42P01": // undefined_table
|
|
358
|
-
return new Error(
|
|
359
|
-
`Table not found for "${collectionSlug}": ${pgMessage}. Check if your schema is up to date (run migrations).${suffix}`
|
|
360
|
-
);
|
|
361
|
-
default: {
|
|
362
|
-
// Unhandled PG code — still surface the actual database message
|
|
363
|
-
const parts = [`Database error in "${collectionSlug}" [${pgError.code}]: ${pgMessage}`];
|
|
364
|
-
if (detail) parts.push(`Detail: ${detail}`);
|
|
365
|
-
if (column) parts.push(`Column: ${column}`);
|
|
366
|
-
if (dataType) parts.push(`Data type: ${dataType}`);
|
|
367
|
-
if (constraint) parts.push(`Constraint: ${constraint}`);
|
|
368
|
-
if (hint) parts.push(`Hint: ${hint}`);
|
|
369
|
-
return new Error(parts.join(". "));
|
|
370
|
-
}
|
|
371
|
-
}
|
|
310
|
+
const { message } = pgErrorToFriendlyMessage(pgError, collectionSlug);
|
|
311
|
+
return new Error(message);
|
|
372
312
|
}
|
|
373
313
|
|
|
374
314
|
// No PG error found — try to extract a useful message from the
|
|
375
315
|
// Drizzle wrapper instead of leaking the raw SQL query + params.
|
|
376
|
-
const causeMessage =
|
|
316
|
+
const causeMessage = extractCauseMessage(error);
|
|
377
317
|
if (causeMessage) {
|
|
378
318
|
return new Error(`Database error in "${collectionSlug}": ${causeMessage}`);
|
|
379
319
|
}
|
|
380
320
|
|
|
381
|
-
// Last resort:
|
|
382
|
-
if (error instanceof Error) {
|
|
383
|
-
|
|
384
|
-
return new Error(cleaned);
|
|
321
|
+
// Last resort: generic message, never leak raw SQL
|
|
322
|
+
if (error instanceof Error && error.message.startsWith("Failed query:")) {
|
|
323
|
+
return new Error(`Failed to save entity in "${collectionSlug}". Check server logs for details.`);
|
|
385
324
|
}
|
|
386
325
|
return new Error(`Database error in "${collectionSlug}": ${String(error)}`);
|
|
387
326
|
}
|
|
388
|
-
|
|
389
|
-
/**
|
|
390
|
-
* Walk the error cause chain and return the deepest meaningful message.
|
|
391
|
-
*/
|
|
392
|
-
private extractCauseMessage(error: unknown): string | null {
|
|
393
|
-
if (!error || typeof error !== "object") return null;
|
|
394
|
-
const err = error as Error & { cause?: unknown };
|
|
395
|
-
|
|
396
|
-
if (err.cause && typeof err.cause === "object") {
|
|
397
|
-
const deeper = this.extractCauseMessage(err.cause);
|
|
398
|
-
if (deeper) return deeper;
|
|
399
|
-
// The cause itself has a message
|
|
400
|
-
if (err.cause instanceof Error && err.cause.message) {
|
|
401
|
-
return err.cause.message;
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
return null;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
/**
|
|
408
|
-
* Strip the raw SQL query from a Drizzle "Failed query: ..." message,
|
|
409
|
-
* keeping only the error description.
|
|
410
|
-
*/
|
|
411
|
-
private stripSqlFromMessage(message: string, collectionSlug: string): string {
|
|
412
|
-
// Drizzle format: "Failed query: <SQL>\nparams: <params>"
|
|
413
|
-
if (message.startsWith("Failed query:")) {
|
|
414
|
-
return `Failed to save entity in "${collectionSlug}". Check server logs for details.`;
|
|
415
|
-
}
|
|
416
|
-
return message;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
/**
|
|
420
|
-
* Extract the underlying PostgreSQL error from a Drizzle wrapper.
|
|
421
|
-
* Drizzle wraps PG errors in a `cause` property.
|
|
422
|
-
*/
|
|
423
|
-
private extractPgError(error: unknown): (Error & { code?: string; detail?: unknown; hint?: unknown; constraint?: unknown; column?: unknown; table?: unknown; dataType?: unknown }) | null {
|
|
424
|
-
if (!error || typeof error !== "object") return null;
|
|
425
|
-
|
|
426
|
-
const err = error as Error & { code?: string; cause?: unknown; detail?: unknown };
|
|
427
|
-
|
|
428
|
-
// Check if the error itself has a PG error code
|
|
429
|
-
if (err.code && /^[0-9A-Z]{5}$/.test(err.code)) {
|
|
430
|
-
return err as Error & { code: string; detail?: unknown; hint?: unknown; constraint?: unknown; column?: unknown; table?: unknown; dataType?: unknown };
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// Check the cause chain (Drizzle wraps PG errors)
|
|
434
|
-
if (err.cause && typeof err.cause === "object") {
|
|
435
|
-
return this.extractPgError(err.cause);
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
return null;
|
|
439
|
-
}
|
|
440
327
|
}
|