@rebasepro/server-postgres 0.13.0 → 0.13.1-canary.g394d868
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/PostgresBackendDriver.d.ts +48 -1
- package/dist/backup-service-CD8o_1Sl.js.map +1 -1
- package/dist/cli-helpers.d.ts +1 -1
- package/dist/{ensure-collection-policies-ViG8XiPn.js → ensure-collection-policies-BTdgHBKV.js} +2 -2
- package/dist/{ensure-collection-policies-ViG8XiPn.js.map → ensure-collection-policies-BTdgHBKV.js.map} +1 -1
- package/dist/{ensure-collection-tables-CBQdOETu.js → ensure-collection-tables-CdRsuy33.js} +33 -7
- package/dist/ensure-collection-tables-CdRsuy33.js.map +1 -0
- package/dist/index.es.js +231 -94
- package/dist/index.es.js.map +1 -1
- package/dist/schema/ensure-collection-tables.d.ts +1 -1
- package/dist/schema/generate-drizzle-schema-logic.d.ts +1 -1
- package/dist/schema/generate-postgres-ddl-logic.d.ts +5 -5
- package/dist/schema/generated-schema-staleness.d.ts +39 -0
- package/dist/services/FetchService.d.ts +10 -7
- package/dist/services/dataService.d.ts +2 -0
- package/dist/services/realtimeService.d.ts +25 -21
- package/dist/{src-DlPBctw_.js → src-B0PGnKU3.js} +174 -16
- package/dist/src-B0PGnKU3.js.map +1 -0
- package/dist/src-DoU9yPqq.js.map +1 -1
- package/package.json +9 -8
- package/src/PostgresBackendDriver.ts +165 -3
- package/src/cli-helpers.ts +8 -2
- package/src/cli.ts +79 -0
- package/src/collections/validate-relations.ts +124 -17
- package/src/data-transformer.ts +13 -3
- package/src/schema/doctor.ts +7 -5
- package/src/schema/ensure-collection-tables.ts +8 -2
- package/src/schema/generate-drizzle-schema-logic.ts +8 -2
- package/src/schema/generate-postgres-ddl-logic.ts +47 -11
- package/src/schema/generated-schema-staleness.ts +169 -0
- package/src/schema/introspect-db-logic.ts +1 -1
- package/src/schema/non-sql-collections.test.ts +131 -0
- package/src/services/FetchService.ts +10 -93
- package/src/services/PersistService.ts +14 -1
- package/src/services/dataService.ts +2 -0
- package/src/services/realtimeService.ts +36 -33
- package/dist/ensure-collection-tables-CBQdOETu.js.map +0 -1
- package/dist/src-DlPBctw_.js.map +0 -1
package/dist/index.es.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createRequire as __createRequire } from "module";
|
|
|
2
2
|
import process from "process";
|
|
3
3
|
__createRequire(import.meta.url);
|
|
4
4
|
import { a as guardPoolAgainstDirtyRelease, i as createReadReplicaConnection, n as createDirectDatabaseConnection, o as pinSearchPath, r as createPostgresDatabaseConnection } from "./connection-BuZ97wsr.js";
|
|
5
|
-
import { A as
|
|
5
|
+
import { A as mergeDeep, C as createRelationRefWithData, D as legacyForeignKeyName, E as generateForeignKeyName, F as resolveClientListLimit, I as hasForeignKeyOnTarget, L as isManyToMany, M as toSnakeCase, O as getPolicyNamesForRule, R as Vector, S as createRelationRef, T as updateDateAutoValues, _ as resolveCollectionRelations, a as getJunctionCollectionConfig, b as isAddressableId, c as getEffectiveSecurityRules, d as findAnonymousGrants, f as findRelation, g as getTableVarName, h as getTableName$1, i as resolveStringColumnLength, j as camelCase, k as isPrototypePollutingKey, l as policyToPostgres, m as getEnumVarName, n as CollectionRegistry, o as getJunctionSecurityRules, p as getColumnName, r as relationalCollections, s as resolveJunctionSpecs, t as buildSdkData, u as securityRuleToConditions, v as buildCompositeId, w as normalizeToEntityRelation, x as parseIdValues, y as getDeclaredPrimaryKeys } from "./src-B0PGnKU3.js";
|
|
6
6
|
import { n as isPostgresCollectionConfig, r as isRelationalCollectionConfig } from "./src-DoU9yPqq.js";
|
|
7
7
|
import { t as ANONYMOUS_USER_ID } from "./policy-CeA1JcxP.js";
|
|
8
8
|
import { t as createPostgresWebSocket } from "./websocket-B2LsrINK.js";
|
|
@@ -30,25 +30,6 @@ import { randomUUID } from "crypto";
|
|
|
30
30
|
function isChannelBusInstance(setting) {
|
|
31
31
|
return typeof setting?.publish === "function";
|
|
32
32
|
}
|
|
33
|
-
/**
|
|
34
|
-
* Resolve a client-supplied list `limit` into a safe, always-defined value.
|
|
35
|
-
*
|
|
36
|
-
* - A provided limit is coerced to an integer and clamped to `[1, maxLimit]`,
|
|
37
|
-
* so `0`, negatives, and absurd values can never bypass the cap.
|
|
38
|
-
* - An absent / blank / non-numeric limit falls back to the mode default:
|
|
39
|
-
* `vectorDefaultLimit` for a vector search, otherwise `defaultLimit`.
|
|
40
|
-
*
|
|
41
|
-
* The return is never `undefined` — no ingress that routes its client limit
|
|
42
|
-
* through this can produce an unbounded read.
|
|
43
|
-
*/
|
|
44
|
-
function resolveClientListLimit(rawLimit, opts = {}) {
|
|
45
|
-
const maxLimit = opts.maxLimit ?? 1e3;
|
|
46
|
-
if (rawLimit != null && String(rawLimit).trim() !== "") {
|
|
47
|
-
const parsed = typeof rawLimit === "number" ? rawLimit : parseInt(String(rawLimit), 10);
|
|
48
|
-
if (Number.isFinite(parsed)) return Math.min(Math.max(1, Math.floor(parsed)), maxLimit);
|
|
49
|
-
}
|
|
50
|
-
return opts.vectorSearch ? opts.vectorDefaultLimit ?? 10 : opts.defaultLimit ?? 50;
|
|
51
|
-
}
|
|
52
33
|
//#endregion
|
|
53
34
|
//#region ../common/src/util/email.ts
|
|
54
35
|
/**
|
|
@@ -1276,7 +1257,11 @@ function sanitizeAndConvertDates(obj) {
|
|
|
1276
1257
|
if (obj instanceof Date) return obj.toISOString();
|
|
1277
1258
|
if (typeof obj === "object") {
|
|
1278
1259
|
const newObj = {};
|
|
1279
|
-
for (const key in obj)
|
|
1260
|
+
for (const key in obj) {
|
|
1261
|
+
if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;
|
|
1262
|
+
if (isPrototypePollutingKey(key)) continue;
|
|
1263
|
+
newObj[key] = sanitizeAndConvertDates(obj[key]);
|
|
1264
|
+
}
|
|
1280
1265
|
return newObj;
|
|
1281
1266
|
}
|
|
1282
1267
|
if (typeof obj === "string") {
|
|
@@ -1305,6 +1290,7 @@ function serializeDataToServer(row, properties, collection, registry) {
|
|
|
1305
1290
|
if (relation.kind === "belongsTo") foreignKeys.add(relation.localKey);
|
|
1306
1291
|
});
|
|
1307
1292
|
for (const [key, value] of Object.entries(row)) {
|
|
1293
|
+
if (isPrototypePollutingKey(key)) continue;
|
|
1308
1294
|
const property = properties[key];
|
|
1309
1295
|
const effectiveValue = foreignKeys.has(key) && value === "" ? null : value;
|
|
1310
1296
|
if (!property) {
|
|
@@ -3796,51 +3782,6 @@ var FetchService = class {
|
|
|
3796
3782
|
return !!this.getQueryBuilder(tableName);
|
|
3797
3783
|
}
|
|
3798
3784
|
/**
|
|
3799
|
-
* Attempt to use Drizzle's relational query API (db.query.<table>.findMany)
|
|
3800
|
-
* for efficient JOIN-based relation loading.
|
|
3801
|
-
* Returns null if the API is not available or the query fails.
|
|
3802
|
-
* Note: Primary path now uses `buildWithConfig` + `buildDrizzleQueryOptions`.
|
|
3803
|
-
*/
|
|
3804
|
-
async fetchWithDrizzleQuery(collectionPath, collection, options, include, idInfo, idInfoArray) {
|
|
3805
|
-
try {
|
|
3806
|
-
const table = getTableForCollection(collection, this.registry);
|
|
3807
|
-
const tableName = getTableName(table);
|
|
3808
|
-
const queryTarget = this.getQueryBuilder(tableName);
|
|
3809
|
-
if (!queryTarget?.findMany) return null;
|
|
3810
|
-
const resolvedRelations = resolveCollectionRelations(collection);
|
|
3811
|
-
const withConfig = {};
|
|
3812
|
-
for (const [key, relation] of Object.entries(resolvedRelations)) if (include[0] === "*" || include.includes(key)) {
|
|
3813
|
-
const drizzleRelName = relation.relationName || key;
|
|
3814
|
-
withConfig[drizzleRelName] = true;
|
|
3815
|
-
}
|
|
3816
|
-
const queryOpts = { with: withConfig };
|
|
3817
|
-
if (options.limit) queryOpts.limit = options.limit;
|
|
3818
|
-
if (options.filter) {
|
|
3819
|
-
const filterConditions = this.buildFilterConditions(options.filter, table, collectionPath);
|
|
3820
|
-
if (filterConditions.length > 0) queryOpts.where = and(...filterConditions);
|
|
3821
|
-
}
|
|
3822
|
-
if (options.orderBy) {
|
|
3823
|
-
const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
|
|
3824
|
-
if (orderByField) queryOpts.orderBy = options.order === "asc" ? asc(orderByField) : desc(orderByField);
|
|
3825
|
-
}
|
|
3826
|
-
return (await queryTarget.findMany(queryOpts)).map((row) => {
|
|
3827
|
-
const flat = {};
|
|
3828
|
-
for (const [k, v] of Object.entries(row)) if (Array.isArray(v)) flat[k] = v.map((item) => {
|
|
3829
|
-
const keys = Object.keys(item);
|
|
3830
|
-
const nestedObj = keys.find((nk) => typeof item[nk] === "object" && item[nk] !== null && !Array.isArray(item[nk]));
|
|
3831
|
-
if (nestedObj && keys.length <= 3) return { ...item[nestedObj] };
|
|
3832
|
-
return { ...item };
|
|
3833
|
-
});
|
|
3834
|
-
else if (typeof v === "object" && v !== null) flat[k] = { ...v };
|
|
3835
|
-
else flat[k] = v;
|
|
3836
|
-
return flat;
|
|
3837
|
-
});
|
|
3838
|
-
} catch (e) {
|
|
3839
|
-
logger.warn(`[include] Drizzle relational query failed for '${collectionPath}', falling back`, { error: e });
|
|
3840
|
-
return null;
|
|
3841
|
-
}
|
|
3842
|
-
}
|
|
3843
|
-
/**
|
|
3844
3785
|
* Fallback path used when db.query is unavailable.
|
|
3845
3786
|
* The primary path uses db.query.findMany with `with` config, which
|
|
3846
3787
|
* loads all relations in a single query.
|
|
@@ -4091,7 +4032,8 @@ var PersistService = class {
|
|
|
4091
4032
|
if (error instanceof ApiError || error?.name === "ApiError") return error;
|
|
4092
4033
|
const pgError = extractPgError(error);
|
|
4093
4034
|
if (pgError) {
|
|
4094
|
-
const { message } = pgErrorToFriendlyMessage(pgError, collectionSlug);
|
|
4035
|
+
const { message, code } = pgErrorToFriendlyMessage(pgError, collectionSlug);
|
|
4036
|
+
if (/^2[23]/.test(code)) return code === "23505" ? ApiError.conflict(message, `PG_${code}`) : ApiError.badRequest(message, `PG_${code}`);
|
|
4095
4037
|
return new Error(message);
|
|
4096
4038
|
}
|
|
4097
4039
|
const causeMessage = extractCauseMessage(error);
|
|
@@ -4749,6 +4691,25 @@ var PostgresBackendDriver = class PostgresBackendDriver {
|
|
|
4749
4691
|
}
|
|
4750
4692
|
};
|
|
4751
4693
|
}
|
|
4694
|
+
/**
|
|
4695
|
+
* Build the context handed to every collection callback.
|
|
4696
|
+
*
|
|
4697
|
+
* Note `data: this.data` — `this` is whichever driver is running the
|
|
4698
|
+
* operation, so the callback's data plane inherits that driver's privilege.
|
|
4699
|
+
* On a user request `AuthenticatedPostgresBackendDriver.withTransaction`
|
|
4700
|
+
* constructs a fresh base driver bound to the RLS-scoped transaction and
|
|
4701
|
+
* runs the operation on it, so `this.data` speaks through that connection
|
|
4702
|
+
* and policies apply. On server-context work `this` is the base driver on
|
|
4703
|
+
* the owner connection, and they do not. Pinned by the
|
|
4704
|
+
* `"scopes context.data to the caller"` case in the `rls-enforcement` e2e
|
|
4705
|
+
* suite, because it is the kind of property that is easy to break from a
|
|
4706
|
+
* distance and impossible to notice.
|
|
4707
|
+
*
|
|
4708
|
+
* Previously returned through `as unknown as RebaseCallContext`, which
|
|
4709
|
+
* disabled checking for the whole object and let `driver` — documented in
|
|
4710
|
+
* the callbacks guide — sit on the runtime context while absent from the
|
|
4711
|
+
* contract. Both are declared now, so this is a plain typed return.
|
|
4712
|
+
*/
|
|
4752
4713
|
buildCallContext() {
|
|
4753
4714
|
return {
|
|
4754
4715
|
user: this.user,
|
|
@@ -5204,6 +5165,106 @@ var PostgresBackendDriver = class PostgresBackendDriver {
|
|
|
5204
5165
|
return saved;
|
|
5205
5166
|
});
|
|
5206
5167
|
}
|
|
5168
|
+
/**
|
|
5169
|
+
* Update many rows through the same pipeline as {@link save}, in one
|
|
5170
|
+
* transaction.
|
|
5171
|
+
*
|
|
5172
|
+
* Structurally the mirror of {@link saveMany} — same tx-bound sub-driver,
|
|
5173
|
+
* same deferred notifications, same per-row error labelling — but it calls
|
|
5174
|
+
* `save` with an explicit `id` and `status: "existing"`, which is precisely
|
|
5175
|
+
* what `saveMany` cannot do: that one passes `status: "new"` and keeps the
|
|
5176
|
+
* key inside `values`, so it inserts or upserts and can never target a
|
|
5177
|
+
* particular row.
|
|
5178
|
+
*
|
|
5179
|
+
* All-or-nothing, so an id matching no row aborts the batch. A partial
|
|
5180
|
+
* update is the outcome with no good recovery: the caller cannot tell which
|
|
5181
|
+
* half landed without re-reading everything.
|
|
5182
|
+
*/
|
|
5183
|
+
async updateMany({ path, updates, collection }) {
|
|
5184
|
+
return this.db.transaction(async (tx) => {
|
|
5185
|
+
const txDriver = new PostgresBackendDriver(tx, this.realtimeService, this.registry, this.user, this.poolManager, this.historyService);
|
|
5186
|
+
txDriver.dataService = new DataService(tx, this.registry);
|
|
5187
|
+
txDriver.client = this.client;
|
|
5188
|
+
txDriver._deferNotifications = this._deferNotifications;
|
|
5189
|
+
txDriver._pendingNotifications = this._pendingNotifications;
|
|
5190
|
+
const saved = [];
|
|
5191
|
+
for (let i = 0; i < updates.length; i++) {
|
|
5192
|
+
const { id, values } = updates[i];
|
|
5193
|
+
try {
|
|
5194
|
+
if (!await txDriver.fetchOne({
|
|
5195
|
+
path,
|
|
5196
|
+
id: String(id),
|
|
5197
|
+
collection
|
|
5198
|
+
})) throw Object.assign(/* @__PURE__ */ new Error(`No row with id ${JSON.stringify(id)}`), {
|
|
5199
|
+
statusCode: 404,
|
|
5200
|
+
code: "NOT_FOUND"
|
|
5201
|
+
});
|
|
5202
|
+
saved.push(await txDriver.save({
|
|
5203
|
+
path,
|
|
5204
|
+
id: String(id),
|
|
5205
|
+
values,
|
|
5206
|
+
collection,
|
|
5207
|
+
status: "existing"
|
|
5208
|
+
}));
|
|
5209
|
+
} catch (error) {
|
|
5210
|
+
throw Object.assign(new Error(`Update ${i} of ${updates.length} (id ${JSON.stringify(id)}) failed: ${error?.message ?? error}`, { cause: error }), {
|
|
5211
|
+
statusCode: error?.statusCode,
|
|
5212
|
+
code: error?.code,
|
|
5213
|
+
name: error?.name
|
|
5214
|
+
});
|
|
5215
|
+
}
|
|
5216
|
+
}
|
|
5217
|
+
return saved;
|
|
5218
|
+
});
|
|
5219
|
+
}
|
|
5220
|
+
/**
|
|
5221
|
+
* Delete many rows in one transaction, running the full delete pipeline —
|
|
5222
|
+
* `beforeDelete`, the delete, `afterDelete` — for each.
|
|
5223
|
+
*
|
|
5224
|
+
* Looping the single-row {@link delete} rather than emitting one
|
|
5225
|
+
* `DELETE ... WHERE id = ANY($1)` is the deliberate choice: a single
|
|
5226
|
+
* statement would be faster and would skip every callback, so a collection
|
|
5227
|
+
* relying on `beforeDelete` to veto or on `afterDelete` to clean up
|
|
5228
|
+
* dependents would behave differently depending on how many rows the caller
|
|
5229
|
+
* happened to delete at once. Same pipeline, one transaction.
|
|
5230
|
+
*/
|
|
5231
|
+
async deleteMany({ path, ids, collection }) {
|
|
5232
|
+
await this.db.transaction(async (tx) => {
|
|
5233
|
+
const txDriver = new PostgresBackendDriver(tx, this.realtimeService, this.registry, this.user, this.poolManager, this.historyService);
|
|
5234
|
+
txDriver.dataService = new DataService(tx, this.registry);
|
|
5235
|
+
txDriver.client = this.client;
|
|
5236
|
+
txDriver._deferNotifications = this._deferNotifications;
|
|
5237
|
+
txDriver._pendingNotifications = this._pendingNotifications;
|
|
5238
|
+
for (let i = 0; i < ids.length; i++) {
|
|
5239
|
+
const id = ids[i];
|
|
5240
|
+
try {
|
|
5241
|
+
const existing = await txDriver.fetchOne({
|
|
5242
|
+
path,
|
|
5243
|
+
id: String(id),
|
|
5244
|
+
collection
|
|
5245
|
+
});
|
|
5246
|
+
if (!existing) throw Object.assign(/* @__PURE__ */ new Error(`No row with id ${JSON.stringify(id)}`), {
|
|
5247
|
+
statusCode: 404,
|
|
5248
|
+
code: "NOT_FOUND"
|
|
5249
|
+
});
|
|
5250
|
+
await txDriver.delete({
|
|
5251
|
+
row: {
|
|
5252
|
+
id: String(id),
|
|
5253
|
+
path,
|
|
5254
|
+
values: existing
|
|
5255
|
+
},
|
|
5256
|
+
collection
|
|
5257
|
+
});
|
|
5258
|
+
} catch (error) {
|
|
5259
|
+
throw Object.assign(new Error(`Delete ${i} of ${ids.length} (id ${JSON.stringify(id)}) failed: ${error?.message ?? error}`, { cause: error }), {
|
|
5260
|
+
statusCode: error?.statusCode,
|
|
5261
|
+
code: error?.code,
|
|
5262
|
+
name: error?.name
|
|
5263
|
+
});
|
|
5264
|
+
}
|
|
5265
|
+
}
|
|
5266
|
+
});
|
|
5267
|
+
}
|
|
5207
5268
|
async delete({ row, collection }) {
|
|
5208
5269
|
const targetPath = row.path;
|
|
5209
5270
|
const targetRow = { ...row.values ?? {} };
|
|
@@ -6168,7 +6229,8 @@ var computeSharedRelationName = (rel, sourceCollection, _collections) => {
|
|
|
6168
6229
|
}
|
|
6169
6230
|
return fallback;
|
|
6170
6231
|
};
|
|
6171
|
-
var generateSchema = async (
|
|
6232
|
+
var generateSchema = async (allCollections, stripPolicies = false) => {
|
|
6233
|
+
const collections = relationalCollections(allCollections);
|
|
6172
6234
|
let schemaContent = "// This file is auto-generated by the Rebase Drizzle generator. Do not edit manually.\n\n";
|
|
6173
6235
|
const hasUuid = collections.some((c) => c.properties && Object.values(c.properties).some((p) => p.type === "string" && (p.autoValue === "uuid" || p.isId === "uuid")));
|
|
6174
6236
|
const hasVector = collections.some((c) => c.properties && Object.values(c.properties).some((p) => p.type === "vector"));
|
|
@@ -7816,23 +7878,18 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
7816
7878
|
path: request.path,
|
|
7817
7879
|
collectionRequest: {
|
|
7818
7880
|
filter: request.filter,
|
|
7881
|
+
logical: request.logical,
|
|
7819
7882
|
orderBy: request.orderBy,
|
|
7820
7883
|
order: request.order,
|
|
7821
7884
|
limit: boundedLimit,
|
|
7885
|
+
offset: request.offset,
|
|
7822
7886
|
startAfter: request.startAfter,
|
|
7823
7887
|
databaseId: request.collection?.databaseId,
|
|
7824
7888
|
searchString: request.searchString
|
|
7825
7889
|
},
|
|
7826
7890
|
authContext
|
|
7827
7891
|
});
|
|
7828
|
-
const rows = await this.fetchCollectionWithAuth(request.path,
|
|
7829
|
-
filter: request.filter,
|
|
7830
|
-
orderBy: request.orderBy,
|
|
7831
|
-
order: request.order,
|
|
7832
|
-
limit: boundedLimit,
|
|
7833
|
-
startAfter: request.startAfter,
|
|
7834
|
-
searchString: request.searchString
|
|
7835
|
-
}, authContext);
|
|
7892
|
+
const rows = await this.fetchCollectionWithAuth(request.path, this._subscriptions.get(subscriptionId).collectionRequest, authContext);
|
|
7836
7893
|
this.sendCollectionUpdate(clientId, subscriptionId, rows, request.path);
|
|
7837
7894
|
} catch (error) {
|
|
7838
7895
|
const sanitized = sanitizeErrorForClient(error, request.path);
|
|
@@ -8016,6 +8073,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
8016
8073
|
});
|
|
8017
8074
|
else fetchedEntities = await txEntityService.fetchCollection(notifyPath, {
|
|
8018
8075
|
filter: collectionRequest.filter,
|
|
8076
|
+
logical: collectionRequest.logical,
|
|
8019
8077
|
orderBy: collectionRequest.orderBy,
|
|
8020
8078
|
order: collectionRequest.order,
|
|
8021
8079
|
limit: collectionRequest.limit,
|
|
@@ -9203,6 +9261,54 @@ var quote = (xs) => Array.from(xs).map((s) => `\`${s}\``).join(", ");
|
|
|
9203
9261
|
/** `on.from` / `on.to` accept a single column or a composite tuple. */
|
|
9204
9262
|
var asColumns = (value) => Array.isArray(value) ? value : [value];
|
|
9205
9263
|
/**
|
|
9264
|
+
* Distinguish "this column name is wrong" from "the generated schema is old".
|
|
9265
|
+
*
|
|
9266
|
+
* They present identically here — a relation asks for a column the registered
|
|
9267
|
+
* table does not have — but they are opposite problems with opposite fixes, and
|
|
9268
|
+
* getting them the wrong way round is how the 0.12 → 0.13 upgrade bricked
|
|
9269
|
+
* projects.
|
|
9270
|
+
*
|
|
9271
|
+
* The registered table is not the database. It comes from the project's
|
|
9272
|
+
* checked-in `backend/src/schema.generated.ts`, and 0.13 changed the rule that
|
|
9273
|
+
* derives foreign-key names: `categories` yields `category_id` where it used to
|
|
9274
|
+
* yield `categorie_id`. Boot-ensure renames the database column to match, so by
|
|
9275
|
+
* the time this runs the *database* is correct and the *generated module* is the
|
|
9276
|
+
* stale one. Reporting "not a column" then points at the wrong artifact, and the
|
|
9277
|
+
* generic fix — "set `through.targetColumn` to one of: …", listing the legacy
|
|
9278
|
+
* name because that is what the stale module still has — talks the reader into
|
|
9279
|
+
* pinning a column that no longer exists.
|
|
9280
|
+
*
|
|
9281
|
+
* So when the wanted name is what the current rule derives, and the table
|
|
9282
|
+
* carries what the *previous* rule would have derived from the same source, say
|
|
9283
|
+
* that instead.
|
|
9284
|
+
*
|
|
9285
|
+
* @param wanted the column the relation asks for
|
|
9286
|
+
* @param available every column the registered table has
|
|
9287
|
+
* @param sources names the default could have been derived from (a slug, a
|
|
9288
|
+
* relation name) — checking against these rather than guessing
|
|
9289
|
+
* backwards from `wanted` keeps the match exact
|
|
9290
|
+
*/
|
|
9291
|
+
function staleCodegenRename(wanted, available, sources) {
|
|
9292
|
+
for (const source of sources) {
|
|
9293
|
+
if (!source) continue;
|
|
9294
|
+
const current = generateForeignKeyName(source);
|
|
9295
|
+
const legacy = legacyForeignKeyName(source);
|
|
9296
|
+
if (current !== wanted || legacy === current) continue;
|
|
9297
|
+
if (available.has(legacy) && !available.has(current)) return {
|
|
9298
|
+
legacy,
|
|
9299
|
+
current
|
|
9300
|
+
};
|
|
9301
|
+
}
|
|
9302
|
+
return null;
|
|
9303
|
+
}
|
|
9304
|
+
/** The shared explanation, so every relation kind reports it identically. */
|
|
9305
|
+
function staleCodegenDefect(table, { legacy, current }) {
|
|
9306
|
+
return {
|
|
9307
|
+
problem: `the generated Drizzle schema still declares \`${legacy}\` on \`${table}\`, but this release derives \`${current}\` — the generated schema predates the foreign-key naming fix and no longer describes the database`,
|
|
9308
|
+
fix: `regenerate it with \`rebase schema generate\` (or \`pnpm run schema:generate\`). The database column has already been renamed for you at boot, so nothing else is needed. To keep \`${legacy}\` instead, name it explicitly on the relation and regenerate.`
|
|
9309
|
+
};
|
|
9310
|
+
}
|
|
9311
|
+
/**
|
|
9206
9312
|
* Relations whose names do not resolve against the registered schema.
|
|
9207
9313
|
*
|
|
9208
9314
|
* Fails open wherever it cannot see enough to be sure — an unregistered source
|
|
@@ -9249,19 +9355,31 @@ function findRelationDefects(collections, registry) {
|
|
|
9249
9355
|
const targetColumns = columnNames(targetTable);
|
|
9250
9356
|
switch (relation.kind) {
|
|
9251
9357
|
case "belongsTo":
|
|
9252
|
-
if (!sourceColumns.has(relation.localKey))
|
|
9253
|
-
|
|
9254
|
-
|
|
9255
|
-
|
|
9256
|
-
|
|
9358
|
+
if (!sourceColumns.has(relation.localKey)) {
|
|
9359
|
+
const stale = staleCodegenRename(relation.localKey, sourceColumns, [relation.relationName, targetCollection.slug]);
|
|
9360
|
+
defects.push(stale ? {
|
|
9361
|
+
...at,
|
|
9362
|
+
...staleCodegenDefect(sourceTableName, stale)
|
|
9363
|
+
} : {
|
|
9364
|
+
...at,
|
|
9365
|
+
problem: `\`localKey: "${relation.localKey}"\` is not a column on \`${sourceTableName}\``,
|
|
9366
|
+
fix: `add the column, or set \`localKey\` to one of: ${quote(sourceColumns)}`
|
|
9367
|
+
});
|
|
9368
|
+
}
|
|
9257
9369
|
break;
|
|
9258
9370
|
case "hasOne":
|
|
9259
9371
|
case "hasMany":
|
|
9260
|
-
if (!targetColumns.has(relation.foreignKeyOnTarget))
|
|
9261
|
-
|
|
9262
|
-
|
|
9263
|
-
|
|
9264
|
-
|
|
9372
|
+
if (!targetColumns.has(relation.foreignKeyOnTarget)) {
|
|
9373
|
+
const stale = staleCodegenRename(relation.foreignKeyOnTarget, targetColumns, [collection.slug]);
|
|
9374
|
+
defects.push(stale ? {
|
|
9375
|
+
...at,
|
|
9376
|
+
...staleCodegenDefect(targetTableName, stale)
|
|
9377
|
+
} : {
|
|
9378
|
+
...at,
|
|
9379
|
+
problem: `\`foreignKeyOnTarget: "${relation.foreignKeyOnTarget}"\` is not a column on the target table \`${targetTableName}\``,
|
|
9380
|
+
fix: `add the column, or set \`foreignKeyOnTarget\` to one of: ${quote(targetColumns)}`
|
|
9381
|
+
});
|
|
9382
|
+
}
|
|
9265
9383
|
if (relation.sourceKey && !sourceColumns.has(relation.sourceKey)) defects.push({
|
|
9266
9384
|
...at,
|
|
9267
9385
|
problem: `\`sourceKey: "${relation.sourceKey}"\` is not a column on \`${sourceTableName}\``,
|
|
@@ -9280,11 +9398,21 @@ function findRelationDefects(collections, registry) {
|
|
|
9280
9398
|
break;
|
|
9281
9399
|
}
|
|
9282
9400
|
const junctionColumns = columnNames(junction);
|
|
9283
|
-
|
|
9284
|
-
|
|
9285
|
-
|
|
9286
|
-
|
|
9287
|
-
|
|
9401
|
+
const derivedFrom = {
|
|
9402
|
+
sourceColumn: [collection.slug],
|
|
9403
|
+
targetColumn: [targetCollection.slug]
|
|
9404
|
+
};
|
|
9405
|
+
for (const [label, column] of [["sourceColumn", sourceColumn], ["targetColumn", targetColumn]]) if (!junctionColumns.has(column)) {
|
|
9406
|
+
const stale = staleCodegenRename(column, junctionColumns, [...derivedFrom[label]]);
|
|
9407
|
+
defects.push(stale ? {
|
|
9408
|
+
...at,
|
|
9409
|
+
...staleCodegenDefect(table, stale)
|
|
9410
|
+
} : {
|
|
9411
|
+
...at,
|
|
9412
|
+
problem: `\`through.${label}: "${column}"\` is not a column on the junction table \`${table}\``,
|
|
9413
|
+
fix: `set \`through.${label}\` to one of: ${quote(junctionColumns)}` + (label === "sourceColumn" ? " — it is the column naming *this* collection" : "")
|
|
9414
|
+
});
|
|
9415
|
+
}
|
|
9288
9416
|
break;
|
|
9289
9417
|
}
|
|
9290
9418
|
case "via": {
|
|
@@ -9356,7 +9484,16 @@ function assertRelationsResolve(collections, registry) {
|
|
|
9356
9484
|
const defects = findRelationDefects(collections, registry);
|
|
9357
9485
|
if (defects.length === 0) return;
|
|
9358
9486
|
const lines = defects.map((d) => ` • ${d.collection}.${d.relationName} (${d.kind})\n ${d.problem}\n fix: ${d.fix}`);
|
|
9359
|
-
throw new Error(`${defects.length} relation${defects.length === 1 ? "" : "s"} cannot resolve against
|
|
9487
|
+
throw new Error(`${defects.length} relation${defects.length === 1 ? "" : "s"} cannot resolve against \`backend/src/schema.generated.ts\`.
|
|
9488
|
+
|
|
9489
|
+
Each of these would return no rows at query time rather than reporting an error, so they are fatal at boot instead.
|
|
9490
|
+
|
|
9491
|
+
If the database was migrated recently — an upgrade, a \`db push\`, a restore — this file is
|
|
9492
|
+
probably older than the schema it describes. Regenerate it before changing anything else:
|
|
9493
|
+
|
|
9494
|
+
rebase schema generate
|
|
9495
|
+
|
|
9496
|
+
If it is already current, then the collection is what disagrees with it:
|
|
9360
9497
|
|
|
9361
9498
|
` + lines.join("\n\n") + "\n");
|
|
9362
9499
|
}
|
|
@@ -12288,7 +12425,7 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
12288
12425
|
*/
|
|
12289
12426
|
async ensureCollectionSchema(collections, driverResult, log) {
|
|
12290
12427
|
const internals = driverResult.internals;
|
|
12291
|
-
const { ensureCollectionTables } = await import("./ensure-collection-tables-
|
|
12428
|
+
const { ensureCollectionTables } = await import("./ensure-collection-tables-CdRsuy33.js");
|
|
12292
12429
|
const plan = await ensureCollectionTables({ async query(text) {
|
|
12293
12430
|
const result = await internals.db.execute(sql.raw(text));
|
|
12294
12431
|
return { rows: result.rows ?? (Array.isArray(result) ? result : []) };
|
|
@@ -12313,7 +12450,7 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
12313
12450
|
*/
|
|
12314
12451
|
async ensureCollectionPolicies(collections, driverResult, log) {
|
|
12315
12452
|
const internals = driverResult.internals;
|
|
12316
|
-
const { ensureCollectionPolicies } = await import("./ensure-collection-policies-
|
|
12453
|
+
const { ensureCollectionPolicies } = await import("./ensure-collection-policies-BTdgHBKV.js");
|
|
12317
12454
|
const outcome = await ensureCollectionPolicies({ async query(text) {
|
|
12318
12455
|
const result = await internals.db.execute(sql.raw(text));
|
|
12319
12456
|
return { rows: result.rows ?? (Array.isArray(result) ? result : []) };
|