@rebasepro/server-postgres 0.12.1-canary.gf5f1d39 → 0.13.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/dist/PostgresBackendDriver.d.ts +1 -1
- package/dist/PostgresBootstrapper.d.ts +25 -1
- package/dist/auth/services.d.ts +21 -0
- package/dist/backup/backup-service.d.ts +10 -1
- package/dist/backup/pg-tools.d.ts +47 -0
- package/dist/backup-service-CD8o_1Sl.js +8999 -0
- package/dist/backup-service-CD8o_1Sl.js.map +1 -0
- package/dist/cli-helpers.d.ts +39 -0
- package/dist/connection-BuZ97wsr.js +250 -0
- package/dist/connection-BuZ97wsr.js.map +1 -0
- package/dist/connection.d.ts +42 -0
- package/dist/ensure-collection-policies-ViG8XiPn.js +57 -0
- package/dist/ensure-collection-policies-ViG8XiPn.js.map +1 -0
- package/dist/ensure-collection-tables-CBQdOETu.js +650 -0
- package/dist/ensure-collection-tables-CBQdOETu.js.map +1 -0
- package/dist/index.es.js +1231 -10031
- package/dist/index.es.js.map +1 -1
- package/dist/policy-CeA1JcxP.js +105 -0
- package/dist/policy-CeA1JcxP.js.map +1 -0
- package/dist/schema/auth-schema.d.ts +83 -144
- package/dist/schema/ensure-collection-policies.d.ts +60 -0
- package/dist/schema/ensure-collection-tables.d.ts +44 -2
- package/dist/schema/generate-postgres-ddl-logic.d.ts +135 -1
- package/dist/schema/introspect-db-constraints.d.ts +57 -0
- package/dist/schema/introspect-db-logic.d.ts +94 -5
- package/dist/schema/introspect-db-queries.d.ts +119 -0
- package/dist/schema/introspect-db-structure.d.ts +263 -0
- package/dist/schema/introspect-db-types.d.ts +11 -0
- package/dist/services/FetchService.d.ts +4 -1
- package/dist/services/RelationService.d.ts +24 -1
- package/dist/services/channel-bus/index.d.ts +1 -7
- package/dist/services/collection-helpers.d.ts +36 -2
- package/dist/services/dataService.d.ts +3 -1
- package/dist/services/row-pipeline.d.ts +1 -1
- package/dist/{src-BbFOPJ1S.js → src-DlPBctw_.js} +299 -173
- package/dist/src-DlPBctw_.js.map +1 -0
- package/dist/{src-Zqwaw3P5.js → src-DoU9yPqq.js} +3 -159
- package/dist/src-DoU9yPqq.js.map +1 -0
- package/dist/utils/connection-string.d.ts +29 -0
- package/dist/utils/drizzle-conditions.d.ts +5 -4
- package/dist/utils/pg-error-utils.d.ts +35 -0
- package/dist/websocket-B2LsrINK.js +530 -0
- package/dist/websocket-B2LsrINK.js.map +1 -0
- package/package.json +14 -14
- package/src/PostgresAdapter.ts +21 -2
- package/src/PostgresBackendDriver.ts +4 -0
- package/src/PostgresBootstrapper.ts +192 -33
- package/src/auth/ensure-tables.ts +164 -9
- package/src/auth/services.ts +24 -2
- package/src/backup/backup-cli.ts +41 -2
- package/src/backup/backup-service.ts +38 -5
- package/src/backup/pg-tools.ts +96 -3
- package/src/cli-helpers.ts +70 -0
- package/src/cli.ts +44 -26
- package/src/collections/validate-relations.ts +15 -0
- package/src/connection.ts +73 -0
- package/src/data-transformer.ts +9 -3
- package/src/databasePoolManager.ts +5 -2
- package/src/schema/auth-schema.ts +30 -19
- package/src/schema/ensure-collection-policies.ts +105 -0
- package/src/schema/ensure-collection-tables.test.ts +105 -9
- package/src/schema/ensure-collection-tables.ts +220 -32
- package/src/schema/generate-drizzle-schema-logic.ts +42 -6
- package/src/schema/generate-postgres-ddl-logic.ts +382 -19
- package/src/schema/introspect-db-constraints.ts +385 -0
- package/src/schema/introspect-db-inference.ts +18 -8
- package/src/schema/introspect-db-logic.ts +385 -71
- package/src/schema/introspect-db-queries.ts +326 -0
- package/src/schema/introspect-db-structure.ts +670 -0
- package/src/schema/introspect-db-types.ts +56 -0
- package/src/schema/introspect-db.ts +37 -80
- package/src/schema/introspect-runtime.test.ts +56 -8
- package/src/schema/introspect-runtime.ts +31 -9
- package/src/security/policy-drift.test.ts +11 -3
- package/src/services/BranchService.ts +66 -28
- package/src/services/FetchService.ts +90 -14
- package/src/services/PersistService.ts +20 -6
- package/src/services/RelationService.ts +249 -48
- package/src/services/channel-bus/index.ts +0 -9
- package/src/services/collection-helpers.ts +69 -3
- package/src/services/dataService.ts +3 -1
- package/src/services/realtimeService.ts +3 -3
- package/src/services/row-pipeline.ts +1 -1
- package/src/utils/connection-string.ts +58 -0
- package/src/utils/drizzle-conditions.ts +31 -6
- package/src/utils/pg-error-utils.ts +65 -0
- package/src/websocket.ts +18 -9
- package/dist/chunk-DSJWtz9O.js +0 -40
- package/dist/ensure-collection-tables-CNTcZGvn.js +0 -304
- package/dist/ensure-collection-tables-CNTcZGvn.js.map +0 -1
- package/dist/src-BbFOPJ1S.js.map +0 -1
- package/dist/src-Zqwaw3P5.js.map +0 -1
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { and, asc, count, desc, eq, getTableName, gt, lt, or, SQL, TableRelationalConfig, TablesRelationalConfig } from "drizzle-orm";
|
|
1
|
+
import { and, asc, count, desc, eq, getTableColumns, getTableName, gt, lt, or, SQL, TableRelationalConfig, TablesRelationalConfig } from "drizzle-orm";
|
|
2
2
|
import { AnyPgColumn, PgTable } from "drizzle-orm/pg-core";
|
|
3
3
|
import { CollectionConfig, FilterValues, ResolvedRelation, LogicalCondition, isManyToMany } from "@rebasepro/types";
|
|
4
4
|
import type { VectorSearchParams } from "@rebasepro/types";
|
|
5
5
|
import { resolveCollectionRelations, findRelation, createRelationRef, createRelationRefWithData } from "@rebasepro/common";
|
|
6
6
|
import { generateForeignKeyName } from "@rebasepro/utils";
|
|
7
|
-
import { DrizzleConditionBuilder, type FilterCompilationOptions } from "../utils/drizzle-conditions";
|
|
7
|
+
import { DrizzleConditionBuilder, getUnknownFilterFieldsMode, type FilterCompilationOptions } from "../utils/drizzle-conditions";
|
|
8
8
|
import {
|
|
9
9
|
getCollectionByPath,
|
|
10
10
|
getTableForCollection,
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
requirePrimaryKeys,
|
|
13
13
|
deriveRowAddress,
|
|
14
14
|
parseIdValues,
|
|
15
|
+
idCanAddressTable,
|
|
15
16
|
buildCompositeId,
|
|
16
17
|
COMPOSITE_ID_SEPARATOR
|
|
17
18
|
} from "./collection-helpers";
|
|
@@ -20,9 +21,10 @@ import { RelationService } from "./RelationService";
|
|
|
20
21
|
import { RelationalQueryBuilder } from "drizzle-orm/pg-core/query-builders/query";
|
|
21
22
|
import { DrizzleClient } from "../interfaces";
|
|
22
23
|
import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
|
|
23
|
-
import {
|
|
24
|
+
import { toFlatRow, toRestRow, isJunctionRelation } from "./row-pipeline";
|
|
24
25
|
import { isNestedPath, resolveNestedPath, type NestedPathHop } from "./nested-path";
|
|
25
|
-
import { logger } from "@rebasepro/server";
|
|
26
|
+
import { ApiError, logger } from "@rebasepro/server";
|
|
27
|
+
import { reachedDatabase } from "../utils/pg-error-utils";
|
|
26
28
|
|
|
27
29
|
/** Type-safe accessor for Drizzle's relational query API via dynamic table name */
|
|
28
30
|
type DbQueryAccessor = Record<string, RelationalQueryBuilder<any, any>> | undefined;
|
|
@@ -129,12 +131,10 @@ export class FetchService {
|
|
|
129
131
|
if (direct) return direct;
|
|
130
132
|
|
|
131
133
|
// Owning relation, resolved: the relation names its own local key.
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (foreignKey) return foreignKey;
|
|
137
|
-
}
|
|
134
|
+
const declaredRelation = collection ? resolveCollectionRelations(collection)[orderBy] : undefined;
|
|
135
|
+
if (declaredRelation?.kind === "belongsTo") {
|
|
136
|
+
const foreignKey = columnAt(declaredRelation.localKey);
|
|
137
|
+
if (foreignKey) return foreignKey;
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
// No collection in hand — the two shapes an owning relation's key takes
|
|
@@ -145,7 +145,61 @@ export class FetchService {
|
|
|
145
145
|
if (foreignKey) return foreignKey;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
|
|
148
|
+
// Nothing resolved. Returning `undefined` is exactly what the docblock
|
|
149
|
+
// above describes: the caller drops the ORDER BY and hands back rows in
|
|
150
|
+
// whatever order Postgres pleases, while the requester believes they
|
|
151
|
+
// are sorted. `?orderBy=titel` answered 200 with unsorted data and no
|
|
152
|
+
// hint that the sort had been ignored.
|
|
153
|
+
//
|
|
154
|
+
// A *filter* naming a field that does not exist is already refused for
|
|
155
|
+
// precisely this reason — it "used to widen results silently". An
|
|
156
|
+
// unresolvable sort field is the same drift between a query and the
|
|
157
|
+
// schema, so it answers the same way and honours the same switch: one
|
|
158
|
+
// knob, because a deployment that wants the lenient behaviour wants it
|
|
159
|
+
// for both.
|
|
160
|
+
const collectionName = collection?.slug ?? collection?.name;
|
|
161
|
+
const onCollection = collectionName ? ` on collection '${collectionName}'` : "";
|
|
162
|
+
|
|
163
|
+
// A declared to-many relation is a different mistake from a typo, and
|
|
164
|
+
// saying "unknown field" about a field the collection plainly declares
|
|
165
|
+
// sends the reader looking for a spelling error that is not there.
|
|
166
|
+
// There is simply no single value per row to order by — `posts.tags` is
|
|
167
|
+
// a set — so no ORDER BY exists to write, with or without a typo.
|
|
168
|
+
if (declaredRelation && declaredRelation.kind !== "belongsTo") {
|
|
169
|
+
throw ApiError.badRequest(
|
|
170
|
+
`Cannot sort by '${orderBy}'${onCollection}: it is a to-many relation ` +
|
|
171
|
+
`(${declaredRelation.kind}), which has no single value per row to order by.`,
|
|
172
|
+
"ORDER_BY_FIELD_NOT_SORTABLE",
|
|
173
|
+
{ field: orderBy, kind: declaredRelation.kind, ...(collectionName && { collection: collectionName }) }
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (getUnknownFilterFieldsMode() === "warn") {
|
|
178
|
+
logger.warn(
|
|
179
|
+
`Sorting by field '${orderBy}'${onCollection}, but it does not exist in the table — ` +
|
|
180
|
+
"the ORDER BY was dropped and these rows are unsorted."
|
|
181
|
+
);
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
let validFields: string[] = [];
|
|
186
|
+
try {
|
|
187
|
+
validFields = Object.keys(getTableColumns(table)).sort();
|
|
188
|
+
} catch {
|
|
189
|
+
// A table stand-in without Drizzle's column symbols — the message
|
|
190
|
+
// is worth less without the list, but not worth failing over.
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
throw ApiError.badRequest(
|
|
194
|
+
`Unknown orderBy field '${orderBy}'${onCollection}` +
|
|
195
|
+
(validFields.length > 0 ? `. Valid fields: ${validFields.join(", ")}` : ""),
|
|
196
|
+
"UNKNOWN_ORDER_BY_FIELD",
|
|
197
|
+
{
|
|
198
|
+
field: orderBy,
|
|
199
|
+
...(collectionName && { collection: collectionName }),
|
|
200
|
+
...(validFields.length > 0 && { validFields })
|
|
201
|
+
}
|
|
202
|
+
);
|
|
149
203
|
}
|
|
150
204
|
|
|
151
205
|
/**
|
|
@@ -536,6 +590,11 @@ idColumn };
|
|
|
536
590
|
throw new Error(`ID field '${idInfo.fieldName}' not found in table for collection '${collectionPath}'`);
|
|
537
591
|
}
|
|
538
592
|
|
|
593
|
+
// An address the key columns cannot hold names no row — the same answer
|
|
594
|
+
// as a well-formed id nobody has. Asking Postgres instead raises 22P02
|
|
595
|
+
// and aborts the transaction around this read.
|
|
596
|
+
if (!idCanAddressTable(id, table, idInfoArray)) return undefined;
|
|
597
|
+
|
|
539
598
|
const parsedIdObj = parseIdValues(id, idInfoArray);
|
|
540
599
|
const parsedId = parsedIdObj[idInfo.fieldName];
|
|
541
600
|
|
|
@@ -555,7 +614,7 @@ idColumn };
|
|
|
555
614
|
|
|
556
615
|
if (!row) return undefined;
|
|
557
616
|
|
|
558
|
-
const flatRow =
|
|
617
|
+
const flatRow = toFlatRow(row, collection, this.registry);
|
|
559
618
|
|
|
560
619
|
// Post-fetch joinPath relations that Drizzle's `with` can't express
|
|
561
620
|
await this.resolveJoinPathRelations<M>(flatRow, collection, collectionPath, parsedId, databaseId);
|
|
@@ -566,6 +625,7 @@ idColumn };
|
|
|
566
625
|
logger.error(`[FetchService] ResolvedRelation inference error for collection '${collectionPath}': ${e.message}`);
|
|
567
626
|
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.");
|
|
568
627
|
}
|
|
628
|
+
if (reachedDatabase(e)) throw e;
|
|
569
629
|
logger.warn(`[FetchService] db.query.findFirst failed for ${collectionPath}, falling back to db.select`, { error: e });
|
|
570
630
|
}
|
|
571
631
|
}
|
|
@@ -682,7 +742,7 @@ idColumn };
|
|
|
682
742
|
const results = await qb.findMany(queryOpts as Parameters<NonNullable<typeof qb>["findMany"]>[0]);
|
|
683
743
|
|
|
684
744
|
const rows = (results as Record<string, unknown>[]).map(row =>
|
|
685
|
-
|
|
745
|
+
toFlatRow(row, collection, this.registry)
|
|
686
746
|
);
|
|
687
747
|
|
|
688
748
|
return rows;
|
|
@@ -691,6 +751,7 @@ idColumn };
|
|
|
691
751
|
logger.error(`[FetchService] ResolvedRelation inference error for collection '${collectionPath}': ${e.message}`);
|
|
692
752
|
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.");
|
|
693
753
|
}
|
|
754
|
+
if (reachedDatabase(e)) throw e;
|
|
694
755
|
logger.warn(`[FetchService] db.query.findMany failed for ${collectionPath}, falling back to db.select`, { error: e });
|
|
695
756
|
}
|
|
696
757
|
}
|
|
@@ -785,7 +846,7 @@ _distance: vectorMeta.distanceSelect }).from(table).$dynamic()
|
|
|
785
846
|
/**
|
|
786
847
|
* Fallback path used when db.query is unavailable.
|
|
787
848
|
*
|
|
788
|
-
* The primary path runs the results through `
|
|
849
|
+
* The primary path runs the results through `toFlatRow`, which maps
|
|
789
850
|
* relations from what drizzle already nested — no query per row. This one
|
|
790
851
|
* has no nesting to read, so it resolves relations itself, in batches.
|
|
791
852
|
*
|
|
@@ -942,6 +1003,7 @@ relatedTo: hop });
|
|
|
942
1003
|
collectionPath: string,
|
|
943
1004
|
options: {
|
|
944
1005
|
filter?: FilterValues<Extract<keyof M, string>>;
|
|
1006
|
+
logical?: LogicalCondition;
|
|
945
1007
|
searchString?: string;
|
|
946
1008
|
databaseId?: string;
|
|
947
1009
|
} = {}
|
|
@@ -973,6 +1035,13 @@ relatedTo: hop });
|
|
|
973
1035
|
if (filterConditions.length > 0) allConditions.push(...filterConditions);
|
|
974
1036
|
}
|
|
975
1037
|
|
|
1038
|
+
if (options.logical) {
|
|
1039
|
+
const logicalCondition = DrizzleConditionBuilder.buildLogicalConditions(
|
|
1040
|
+
options.logical, table, effectivePath, this.filterContext(effectivePath, table)
|
|
1041
|
+
);
|
|
1042
|
+
if (logicalCondition) allConditions.push(logicalCondition);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
976
1045
|
if (allConditions.length > 0) {
|
|
977
1046
|
const finalCondition = DrizzleConditionBuilder.combineConditionsWithAnd(allConditions);
|
|
978
1047
|
if (finalCondition) query = query.where(finalCondition);
|
|
@@ -1043,6 +1112,8 @@ relatedTo: hop });
|
|
|
1043
1112
|
collectionPath: string,
|
|
1044
1113
|
options: {
|
|
1045
1114
|
filter?: FilterValues<Extract<keyof M, string>>;
|
|
1115
|
+
/** An `or(...)`/`and(...)` group, applied alongside `filter`. */
|
|
1116
|
+
logical?: LogicalCondition;
|
|
1046
1117
|
orderBy?: string;
|
|
1047
1118
|
order?: "desc" | "asc";
|
|
1048
1119
|
limit?: number;
|
|
@@ -1111,6 +1182,7 @@ relatedTo: hop }, include
|
|
|
1111
1182
|
logger.error(`[FetchService] ResolvedRelation inference error for collection '${collectionPath}': ${e.message}`);
|
|
1112
1183
|
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.");
|
|
1113
1184
|
}
|
|
1185
|
+
if (reachedDatabase(e)) throw e;
|
|
1114
1186
|
logger.warn(`[fetchCollectionForRest] db.query.findMany failed for ${collectionPath}, falling back`, { error: e });
|
|
1115
1187
|
}
|
|
1116
1188
|
}
|
|
@@ -1184,6 +1256,9 @@ relatedTo: hop }, include
|
|
|
1184
1256
|
const idInfo = idInfoArray[0];
|
|
1185
1257
|
const idField = table[idInfo.fieldName as keyof typeof table] as AnyPgColumn;
|
|
1186
1258
|
|
|
1259
|
+
// See `fetchOne`: an unaddressable id is a 404, not a database error.
|
|
1260
|
+
if (!idCanAddressTable(id, table, idInfoArray)) return null;
|
|
1261
|
+
|
|
1187
1262
|
const parsedIdObj = parseIdValues(id, idInfoArray);
|
|
1188
1263
|
const parsedId = parsedIdObj[idInfo.fieldName];
|
|
1189
1264
|
|
|
@@ -1217,6 +1292,7 @@ relatedTo: hop }, include
|
|
|
1217
1292
|
logger.error(`[FetchService] ResolvedRelation inference error for collection '${collectionPath}': ${e.message}`);
|
|
1218
1293
|
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.");
|
|
1219
1294
|
}
|
|
1295
|
+
if (reachedDatabase(e)) throw e;
|
|
1220
1296
|
logger.warn(`[fetchOneForRest] db.query.findFirst failed for ${collectionPath}, falling back`, { error: e });
|
|
1221
1297
|
}
|
|
1222
1298
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { eq, and, sql, SQL } from "drizzle-orm";
|
|
2
2
|
import { AnyPgColumn, PgTable } from "drizzle-orm/pg-core";
|
|
3
3
|
// import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
4
|
-
import { CollectionConfig, Properties, ResolvedRelation, type ResolvedManyToMany, isManyToMany } from "@rebasepro/types";
|
|
4
|
+
import { CollectionConfig, Properties, ResolvedRelation, type ResolvedManyToMany, isManyToMany, hasForeignKeyOnTarget } from "@rebasepro/types";
|
|
5
5
|
import { getTableName, resolveCollectionRelations } from "@rebasepro/common";
|
|
6
6
|
import { DrizzleConditionBuilder } from "../utils/drizzle-conditions";
|
|
7
7
|
import {
|
|
@@ -240,16 +240,30 @@ export class PersistService {
|
|
|
240
240
|
throw ApiError.notFound(`No row "${id}" in "${collectionPath}" to update.`);
|
|
241
241
|
}
|
|
242
242
|
} else {
|
|
243
|
-
// One-to-many create: stamp the parent's
|
|
243
|
+
// One-to-many create: stamp the parent's key onto the child's FK.
|
|
244
244
|
const targetColumnName = this.resolveParentForeignKeyColumn(hop);
|
|
245
245
|
|
|
246
246
|
if (targetColumnName) {
|
|
247
|
-
|
|
247
|
+
// Not necessarily the id in the path: a link with a
|
|
248
|
+
// `sourceKey` is pointed at a column on the parent row, and
|
|
249
|
+
// stamping the id would create a child that belongs to
|
|
250
|
+
// nobody — a row the read path would never return again.
|
|
251
|
+
const parentKeyValue = hasForeignKeyOnTarget(hop.relation)
|
|
252
|
+
? await this.relationService.parentKeyValue(hop.parentCollection, hop.relation, hop.parentId)
|
|
253
|
+
: parentIdForWrite();
|
|
254
|
+
|
|
255
|
+
if (parentKeyValue === undefined) {
|
|
256
|
+
throw ApiError.badRequest(
|
|
257
|
+
`Cannot create under "${collectionPath}": the parent row has no value in ` +
|
|
258
|
+
`\`sourceKey\`, so the new row has nothing to point at.`
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
248
262
|
const existingValue = (effectiveValues as Record<string, unknown>)[targetColumnName];
|
|
249
|
-
if (existingValue !== undefined && existingValue !== null && existingValue !==
|
|
250
|
-
logger.warn(`Overriding provided value '${existingValue}' for FK '${targetColumnName}' with path parent
|
|
263
|
+
if (existingValue !== undefined && existingValue !== null && existingValue !== parentKeyValue) {
|
|
264
|
+
logger.warn(`Overriding provided value '${existingValue}' for FK '${targetColumnName}' with path parent key '${parentKeyValue}'.`);
|
|
251
265
|
}
|
|
252
|
-
(effectiveValues as Record<string, unknown>)[targetColumnName] =
|
|
266
|
+
(effectiveValues as Record<string, unknown>)[targetColumnName] = parentKeyValue;
|
|
253
267
|
}
|
|
254
268
|
}
|
|
255
269
|
}
|