@rebasepro/server-postgres 0.11.1-canary.gfd39654 → 0.12.1-canary.gdfba2a1
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/PostgresBootstrapper.d.ts +8 -0
- package/dist/auth/services.d.ts +16 -0
- package/dist/backup-service-DH9kPg-E.js +8866 -0
- package/dist/backup-service-DH9kPg-E.js.map +1 -0
- package/dist/collections/buildRegistry.d.ts +1 -1
- package/dist/connection-B5Wndbr1.js +196 -0
- package/dist/connection-B5Wndbr1.js.map +1 -0
- package/dist/{ensure-collection-tables-DGMYK0fr.js → ensure-collection-tables-CIHSH962.js} +5 -5
- package/dist/ensure-collection-tables-CIHSH962.js.map +1 -0
- package/dist/history/HistoryService.d.ts +9 -29
- package/dist/index.es.js +698 -9604
- package/dist/index.es.js.map +1 -1
- package/dist/schema/auth-schema.d.ts +83 -144
- package/dist/schema/dynamic-tables.d.ts +1 -1
- package/dist/schema/introspect-runtime.d.ts +1 -1
- package/dist/services/FetchService.d.ts +36 -1
- package/dist/services/row-pipeline.d.ts +3 -1
- package/dist/{src-3VmUJ8Xn.js → src-DihrDFuP.js} +354 -165
- package/dist/src-DihrDFuP.js.map +1 -0
- package/dist/{src-D5xBTl32.js → src-DoU9yPqq.js} +79 -189
- package/dist/src-DoU9yPqq.js.map +1 -0
- package/dist/utils/drizzle-conditions.d.ts +157 -3
- package/dist/utils/pg-error-utils.d.ts +6 -3
- package/dist/websocket-BKcGvILX.js +528 -0
- package/dist/websocket-BKcGvILX.js.map +1 -0
- package/package.json +14 -14
- package/src/PostgresBootstrapper.ts +23 -6
- package/src/auth/ensure-tables.ts +164 -9
- package/src/auth/services.ts +21 -2
- package/src/collections/buildRegistry.ts +1 -1
- package/src/history/HistoryService.ts +13 -31
- package/src/schema/auth-schema.ts +30 -19
- package/src/schema/dynamic-tables.ts +1 -1
- package/src/schema/generate-drizzle-schema-logic.ts +17 -5
- package/src/schema/generate-postgres-ddl-logic.ts +7 -3
- package/src/schema/introspect-runtime.ts +1 -1
- package/src/services/FetchService.ts +79 -11
- package/src/services/row-pipeline.ts +3 -1
- package/src/utils/drizzle-conditions.ts +509 -45
- package/src/utils/pg-error-utils.ts +52 -3
- package/dist/chunk-DSJWtz9O.js +0 -40
- package/dist/ensure-collection-tables-DGMYK0fr.js.map +0 -1
- package/dist/src-3VmUJ8Xn.js.map +0 -1
- package/dist/src-D5xBTl32.js.map +0 -1
|
@@ -1,8 +1,19 @@
|
|
|
1
|
-
import { pgSchema, pgTable,
|
|
1
|
+
import { pgSchema, pgTable, uuid, timestamp, boolean, jsonb, text, unique, index } from "drizzle-orm/pg-core";
|
|
2
2
|
import { relations } from "drizzle-orm";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Factory function to dynamically create the auth tables bound to the specified schema names.
|
|
6
|
+
*
|
|
7
|
+
* This module builds queries; it does not create tables. `ensureAuthTablesExist`
|
|
8
|
+
* owns the DDL, which makes everything here a *claim* about a database it cannot
|
|
9
|
+
* enforce — and the claims drifted. Every column below was declared
|
|
10
|
+
* `varchar(n)` while the DDL created it as `TEXT`: `user_agent` as varchar(500),
|
|
11
|
+
* `ip_address` as varchar(45), `secret_encrypted` as varchar(500), every
|
|
12
|
+
* `token_hash` as varchar(255). None of it was true of any database this
|
|
13
|
+
* framework ever provisioned. Harmless at runtime — drizzle does not enforce a
|
|
14
|
+
* length client-side, so the widths only ever misled the next reader — but a
|
|
15
|
+
* schema module that describes columns that do not exist is worse than no
|
|
16
|
+
* schema module. They are `text` here now because they are TEXT there.
|
|
6
17
|
*/
|
|
7
18
|
export function createAuthSchema(usersSchemaName = "rebase") {
|
|
8
19
|
const usersSchema = usersSchemaName === "public" ? null : pgSchema(usersSchemaName);
|
|
@@ -15,12 +26,12 @@ export function createAuthSchema(usersSchemaName = "rebase") {
|
|
|
15
26
|
*/
|
|
16
27
|
const users = usersTableCreator("users", {
|
|
17
28
|
id: uuid("id").defaultRandom().primaryKey(),
|
|
18
|
-
email:
|
|
19
|
-
passwordHash:
|
|
20
|
-
displayName:
|
|
21
|
-
photoUrl:
|
|
29
|
+
email: text("email").notNull().unique(),
|
|
30
|
+
passwordHash: text("password_hash"), // NULL for OAuth-only users
|
|
31
|
+
displayName: text("display_name"),
|
|
32
|
+
photoUrl: text("photo_url"),
|
|
22
33
|
emailVerified: boolean("email_verified").default(false).notNull(),
|
|
23
|
-
emailVerificationToken:
|
|
34
|
+
emailVerificationToken: text("email_verification_token"),
|
|
24
35
|
emailVerificationSentAt: timestamp("email_verification_sent_at"),
|
|
25
36
|
isAnonymous: boolean("is_anonymous").default(false).notNull(),
|
|
26
37
|
roles: text("roles").array().default([]).notNull(),
|
|
@@ -65,7 +76,7 @@ export function createAuthSchema(usersSchemaName = "rebase") {
|
|
|
65
76
|
id: uuid("id").defaultRandom().primaryKey(),
|
|
66
77
|
uid: uuid("uid").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
67
78
|
sessionId: uuid("session_id").defaultRandom().notNull(),
|
|
68
|
-
tokenHash:
|
|
79
|
+
tokenHash: text("token_hash").notNull().unique(),
|
|
69
80
|
expiresAt: timestamp("expires_at").notNull(),
|
|
70
81
|
revoked: boolean("revoked").default(false).notNull(),
|
|
71
82
|
rotatedAt: timestamp("rotated_at"),
|
|
@@ -76,8 +87,8 @@ export function createAuthSchema(usersSchemaName = "rebase") {
|
|
|
76
87
|
* that rotates immediately after it.
|
|
77
88
|
*/
|
|
78
89
|
sessionStartedAt: timestamp("session_started_at").defaultNow().notNull(),
|
|
79
|
-
userAgent:
|
|
80
|
-
ipAddress:
|
|
90
|
+
userAgent: text("user_agent"),
|
|
91
|
+
ipAddress: text("ip_address"),
|
|
81
92
|
createdAt: timestamp("created_at").defaultNow().notNull()
|
|
82
93
|
}, (table) => ({
|
|
83
94
|
sessionIdx: index("idx_refresh_tokens_session").on(table.sessionId)
|
|
@@ -89,7 +100,7 @@ export function createAuthSchema(usersSchemaName = "rebase") {
|
|
|
89
100
|
const passwordResetTokens = tableCreator("password_reset_tokens", {
|
|
90
101
|
id: uuid("id").defaultRandom().primaryKey(),
|
|
91
102
|
uid: uuid("uid").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
92
|
-
tokenHash:
|
|
103
|
+
tokenHash: text("token_hash").notNull().unique(),
|
|
93
104
|
expiresAt: timestamp("expires_at").notNull(),
|
|
94
105
|
usedAt: timestamp("used_at"),
|
|
95
106
|
createdAt: timestamp("created_at").defaultNow().notNull()
|
|
@@ -99,7 +110,7 @@ export function createAuthSchema(usersSchemaName = "rebase") {
|
|
|
99
110
|
* App config - key/value store for custom settings
|
|
100
111
|
*/
|
|
101
112
|
const appConfig = tableCreator("app_config", {
|
|
102
|
-
key:
|
|
113
|
+
key: text("key").primaryKey(),
|
|
103
114
|
value: jsonb("value").notNull(),
|
|
104
115
|
updatedAt: timestamp("updated_at").defaultNow().notNull()
|
|
105
116
|
});
|
|
@@ -110,8 +121,8 @@ export function createAuthSchema(usersSchemaName = "rebase") {
|
|
|
110
121
|
const userIdentities = tableCreator("user_identities", {
|
|
111
122
|
id: uuid("id").defaultRandom().primaryKey(),
|
|
112
123
|
uid: uuid("uid").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
113
|
-
provider:
|
|
114
|
-
providerId:
|
|
124
|
+
provider: text("provider").notNull(), // e.g. 'google', 'linkedin'
|
|
125
|
+
providerId: text("provider_id").notNull(),
|
|
115
126
|
profileData: jsonb("profile_data"),
|
|
116
127
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
117
128
|
updatedAt: timestamp("updated_at").defaultNow().notNull()
|
|
@@ -125,9 +136,9 @@ export function createAuthSchema(usersSchemaName = "rebase") {
|
|
|
125
136
|
const mfaFactors = tableCreator("mfa_factors", {
|
|
126
137
|
id: uuid("id").defaultRandom().primaryKey(),
|
|
127
138
|
uid: uuid("uid").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
128
|
-
factorType:
|
|
129
|
-
secretEncrypted:
|
|
130
|
-
friendlyName:
|
|
139
|
+
factorType: text("factor_type").notNull(), // 'totp'
|
|
140
|
+
secretEncrypted: text("secret_encrypted").notNull(),
|
|
141
|
+
friendlyName: text("friendly_name"),
|
|
131
142
|
verified: boolean("verified").default(false).notNull(),
|
|
132
143
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
133
144
|
updatedAt: timestamp("updated_at").defaultNow().notNull()
|
|
@@ -141,7 +152,7 @@ export function createAuthSchema(usersSchemaName = "rebase") {
|
|
|
141
152
|
factorId: uuid("factor_id").notNull().references(() => mfaFactors.id, { onDelete: "cascade" }),
|
|
142
153
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
143
154
|
verifiedAt: timestamp("verified_at"),
|
|
144
|
-
ipAddress:
|
|
155
|
+
ipAddress: text("ip_address"),
|
|
145
156
|
expiresAt: timestamp("expires_at").notNull()
|
|
146
157
|
});
|
|
147
158
|
|
|
@@ -151,7 +162,7 @@ export function createAuthSchema(usersSchemaName = "rebase") {
|
|
|
151
162
|
const recoveryCodes = tableCreator("recovery_codes", {
|
|
152
163
|
id: uuid("id").defaultRandom().primaryKey(),
|
|
153
164
|
uid: uuid("uid").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
154
|
-
codeHash:
|
|
165
|
+
codeHash: text("code_hash").notNull(),
|
|
155
166
|
usedAt: timestamp("used_at"),
|
|
156
167
|
createdAt: timestamp("created_at").defaultNow().notNull()
|
|
157
168
|
});
|
|
@@ -162,7 +173,7 @@ export function createAuthSchema(usersSchemaName = "rebase") {
|
|
|
162
173
|
const magicLinkTokens = tableCreator("magic_link_tokens", {
|
|
163
174
|
id: uuid("id").defaultRandom().primaryKey(),
|
|
164
175
|
uid: uuid("uid").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
165
|
-
tokenHash:
|
|
176
|
+
tokenHash: text("token_hash").notNull().unique(),
|
|
166
177
|
expiresAt: timestamp("expires_at").notNull(),
|
|
167
178
|
usedAt: timestamp("used_at"),
|
|
168
179
|
createdAt: timestamp("created_at").defaultNow().notNull()
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Build drizzle tables at runtime from an introspected schema.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* A project with declared collections gets its drizzle tables from a generated `schema.generated.ts` that
|
|
5
5
|
* the developer commits. BaaS mode has no such file — it points at a database
|
|
6
6
|
* and serves it — so the equivalent table objects are constructed here from
|
|
7
7
|
* `information_schema` metadata.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CollectionConfig, NumberProperty, Property, ResolvedRelation, RelationProperty, SecurityOperation, SecurityRule, StringProperty, isPostgresCollectionConfig, DateProperty, ArrayProperty, MapProperty, ReferenceProperty, VectorProperty, BinaryProperty, isManyToMany, type ResolvedManyToMany, type ResolvedBelongsTo, type ResolvedForeignKeyOnTarget, hasForeignKeyOnTarget } from "@rebasepro/types";
|
|
2
2
|
import { getPrimaryKeys } from "../services/collection-helpers";
|
|
3
|
-
import { getEnumVarName, getTableName, getTableVarName, resolveCollectionRelations, findRelation, securityRuleToConditions, policyToPostgres, getEffectiveSecurityRules, resolveJunctionSpecs, getJunctionSecurityRules, getJunctionCollectionConfig } from "@rebasepro/common";
|
|
3
|
+
import { getEnumVarName, getTableName, getTableVarName, resolveCollectionRelations, findRelation, securityRuleToConditions, policyToPostgres, getEffectiveSecurityRules, resolveJunctionSpecs, getJunctionSecurityRules, getJunctionCollectionConfig, resolveStringColumnLength } from "@rebasepro/common";
|
|
4
4
|
import { toSnakeCase, getPolicyNamesForRule } from "@rebasepro/utils";
|
|
5
5
|
import { logger } from "@rebasepro/server";
|
|
6
6
|
// --- Helper Functions ---
|
|
@@ -98,9 +98,13 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: Collecti
|
|
|
98
98
|
} else if (stringProp.columnType === "uuid") {
|
|
99
99
|
columnDefinition = `uuid("${colName}")`;
|
|
100
100
|
} else if (stringProp.columnType === "char") {
|
|
101
|
-
columnDefinition = `char("${colName}")`;
|
|
101
|
+
columnDefinition = `char("${colName}", { length: ${resolveStringColumnLength(stringProp)} })`;
|
|
102
102
|
} else if (stringProp.columnType === "varchar") {
|
|
103
|
-
|
|
103
|
+
// The length is not optional decoration: `varchar("col")` with
|
|
104
|
+
// no length is an UNBOUNDED varchar in Postgres, which is what
|
|
105
|
+
// this emitted while the DDL generator emitted VARCHAR(255) for
|
|
106
|
+
// the very same property.
|
|
107
|
+
columnDefinition = `varchar("${colName}", { length: ${resolveStringColumnLength(stringProp)} })`;
|
|
104
108
|
} else {
|
|
105
109
|
// `text` is the default, and the only length-unbounded choice.
|
|
106
110
|
// Ask for `varchar` explicitly if you want the length constraint.
|
|
@@ -708,9 +712,17 @@ export const generateSchema = async (collections: CollectionConfig[], stripPolic
|
|
|
708
712
|
emittedRelationNames.add(deduplicationKey);
|
|
709
713
|
|
|
710
714
|
switch (rel.kind) {
|
|
711
|
-
case "belongsTo":
|
|
712
|
-
|
|
715
|
+
case "belongsTo": {
|
|
716
|
+
// `localKey` is a COLUMN name; the generated Drizzle
|
|
717
|
+
// object is keyed by PROPERTY. They differ whenever
|
|
718
|
+
// the property is camelCase — `user_id` is exposed
|
|
719
|
+
// as `userId` — and emitting the column produces a
|
|
720
|
+
// schema that does not compile. The three other
|
|
721
|
+
// emission sites normalise; this one did not.
|
|
722
|
+
const localFieldKey = resolvePropertyKeyForColumn(collection, rel.localKey);
|
|
723
|
+
tableRelations.push(` "${relationKey}": one(${targetTableVar}, {\n fields: [${tableVarName}.${localFieldKey}],\n references: [${targetTableVar}.${getPrimaryKeyName(target)}],\n relationName: \"${drizzleRelationName}\"\n })`);
|
|
713
724
|
break;
|
|
725
|
+
}
|
|
714
726
|
|
|
715
727
|
case "hasOne":
|
|
716
728
|
// The foreign key lives on the TARGET table. Drizzle pairs
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CollectionConfig, NumberProperty, Property, ResolvedRelation, RelationProperty, SecurityOperation, SecurityRule, StringProperty, isPostgresCollectionConfig, DateProperty, ArrayProperty, MapProperty, ReferenceProperty, VectorProperty, BinaryProperty, isManyToMany, type ResolvedManyToMany, type ResolvedBelongsTo } from "@rebasepro/types";
|
|
2
|
-
import { getEnumVarName, getTableName, resolveCollectionRelations, findRelation, securityRuleToConditions, policyToPostgres, getEffectiveSecurityRules, getInjectedSecurityRules, resolveJunctionSpecs, getJunctionSecurityRules, getJunctionCollectionConfig } from "@rebasepro/common";
|
|
2
|
+
import { getEnumVarName, getTableName, resolveCollectionRelations, findRelation, securityRuleToConditions, policyToPostgres, getEffectiveSecurityRules, getInjectedSecurityRules, resolveJunctionSpecs, getJunctionSecurityRules, getJunctionCollectionConfig, resolveStringColumnLength } from "@rebasepro/common";
|
|
3
3
|
import { toSnakeCase, getPolicyNamesForRule } from "@rebasepro/utils";
|
|
4
4
|
|
|
5
5
|
// --- Helper Functions ---
|
|
@@ -103,11 +103,15 @@ export const getSqlColumnType = (propName: string, prop: Property, collection: C
|
|
|
103
103
|
if (stringProp.isId === "uuid" || stringProp.columnType === "uuid") {
|
|
104
104
|
return "UUID";
|
|
105
105
|
}
|
|
106
|
+
// Width comes from `validation.max` when the property states one.
|
|
107
|
+
// It used to be a hardcoded 255 here and *absent* on the Drizzle
|
|
108
|
+
// path, so the same property produced a bounded column down one
|
|
109
|
+
// generator and an unbounded one down the other.
|
|
106
110
|
if (stringProp.columnType === "char") {
|
|
107
|
-
return
|
|
111
|
+
return `CHAR(${resolveStringColumnLength(stringProp)})`;
|
|
108
112
|
}
|
|
109
113
|
if (stringProp.columnType === "varchar") {
|
|
110
|
-
return
|
|
114
|
+
return `VARCHAR(${resolveStringColumnLength(stringProp)})`;
|
|
111
115
|
}
|
|
112
116
|
// `text` is the default. The two generators disagreed here before:
|
|
113
117
|
// this one emitted VARCHAR(255) while the drizzle path emitted a bare
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* single config file.
|
|
8
8
|
*
|
|
9
9
|
* Distinct from `introspect-db.ts`, which runs the same queries but emits
|
|
10
|
-
* TypeScript *source* for a developer to edit and commit (
|
|
10
|
+
* TypeScript *source* for a developer to edit and commit (declared collections). The two
|
|
11
11
|
* share the mapping helpers in `introspect-db-logic.ts` so a table is described
|
|
12
12
|
* the same way whether it was generated or introspected.
|
|
13
13
|
*/
|
|
@@ -3,10 +3,12 @@ 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
|
-
import {
|
|
6
|
+
import { generateForeignKeyName } from "@rebasepro/utils";
|
|
7
|
+
import { DrizzleConditionBuilder, type FilterCompilationOptions } from "../utils/drizzle-conditions";
|
|
7
8
|
import {
|
|
8
9
|
getCollectionByPath,
|
|
9
10
|
getTableForCollection,
|
|
11
|
+
getPrimaryKeys,
|
|
10
12
|
requirePrimaryKeys,
|
|
11
13
|
deriveRowAddress,
|
|
12
14
|
parseIdValues,
|
|
@@ -45,6 +47,45 @@ export class FetchService {
|
|
|
45
47
|
return query?.[tableName] as RelationalQueryBuilder<TablesRelationalConfig, TableRelationalConfig> | undefined;
|
|
46
48
|
}
|
|
47
49
|
|
|
50
|
+
/**
|
|
51
|
+
* The context the condition builder needs to compile a filter key that is
|
|
52
|
+
* not a column name outright.
|
|
53
|
+
*
|
|
54
|
+
* Two such keys. An owning relation's key resolves through the collection's
|
|
55
|
+
* relations to its foreign-key column; a relation whose link lives on the
|
|
56
|
+
* target table or in a junction resolves to a correlated `EXISTS`, which
|
|
57
|
+
* needs the registry to reach that other table and this table's key column
|
|
58
|
+
* to correlate back.
|
|
59
|
+
*
|
|
60
|
+
* Looked up rather than passed: every read path already has the path, only
|
|
61
|
+
* some have the collection, and a path that names no registered collection
|
|
62
|
+
* (a nested/derived one) is not an error here — the builder simply falls
|
|
63
|
+
* back to guessing the default key shapes, and a relation filter it cannot
|
|
64
|
+
* compile stays unresolvable and so fails closed.
|
|
65
|
+
*/
|
|
66
|
+
private filterContext(collectionPath: string, table: PgTable<any>): FilterCompilationOptions {
|
|
67
|
+
const collection = this.registry.getCollectionByPath(collectionPath) ?? undefined;
|
|
68
|
+
return {
|
|
69
|
+
collection,
|
|
70
|
+
registry: this.registry,
|
|
71
|
+
sourceIdColumn: collection ? this.resolveIdColumn(collection, table) : undefined
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The table column this collection's rows are keyed by, or `undefined`.
|
|
77
|
+
*
|
|
78
|
+
* `getPrimaryKeys` rather than `requirePrimaryKeys`: a collection with no
|
|
79
|
+
* resolvable key is not an error on the filter path — it only means the
|
|
80
|
+
* relation filters that would correlate on it cannot be compiled, which
|
|
81
|
+
* the builder already handles by failing that field closed.
|
|
82
|
+
*/
|
|
83
|
+
private resolveIdColumn(collection: CollectionConfig, table: PgTable<any>): AnyPgColumn | undefined {
|
|
84
|
+
const [idInfo] = getPrimaryKeys(collection, this.registry);
|
|
85
|
+
if (!idInfo) return undefined;
|
|
86
|
+
return table[idInfo.fieldName as keyof typeof table] as AnyPgColumn | undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
48
89
|
/**
|
|
49
90
|
* Build filter conditions from FilterValues
|
|
50
91
|
* Delegates to DrizzleConditionBuilder.buildFilterConditions
|
|
@@ -54,7 +95,9 @@ export class FetchService {
|
|
|
54
95
|
table: PgTable<any>,
|
|
55
96
|
collectionPath: string
|
|
56
97
|
): SQL[] {
|
|
57
|
-
return DrizzleConditionBuilder.buildFilterConditions(
|
|
98
|
+
return DrizzleConditionBuilder.buildFilterConditions(
|
|
99
|
+
filter, table, collectionPath, this.filterContext(collectionPath, table)
|
|
100
|
+
);
|
|
58
101
|
}
|
|
59
102
|
|
|
60
103
|
// =============================================================
|
|
@@ -64,20 +107,45 @@ export class FetchService {
|
|
|
64
107
|
/**
|
|
65
108
|
* Resolves the correct Drizzle column for sorting.
|
|
66
109
|
* Automatically maps owning relation property keys to their underlying foreign key column.
|
|
110
|
+
*
|
|
111
|
+
* The relation's own `localKey` is the authority for that foreign key, not
|
|
112
|
+
* `<field>_id`. The default local key comes from `generateForeignKeyName`,
|
|
113
|
+
* which snake-cases *and singularises* — `userProfile` → `user_profile_id`,
|
|
114
|
+
* `users` → `user_id` — and an author can override it outright. A wrong
|
|
115
|
+
* guess resolves to nothing, the caller drops the `ORDER BY`, and the rows
|
|
116
|
+
* come back in whatever order Postgres pleases: paging over that repeats
|
|
117
|
+
* and skips rows rather than erroring. The guesses stay, last, for a
|
|
118
|
+
* caller that hands over no collection to resolve against.
|
|
67
119
|
*/
|
|
68
120
|
private resolveOrderByField(
|
|
69
121
|
table: PgTable<any>,
|
|
70
122
|
orderBy: string,
|
|
71
123
|
collection?: CollectionConfig
|
|
72
124
|
): AnyPgColumn | undefined {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
125
|
+
const columnAt = (key: string): AnyPgColumn | undefined =>
|
|
126
|
+
(key in table ? table[key as keyof typeof table] as AnyPgColumn : undefined) || undefined;
|
|
127
|
+
|
|
128
|
+
const direct = columnAt(orderBy);
|
|
129
|
+
if (direct) return direct;
|
|
130
|
+
|
|
131
|
+
// Owning relation, resolved: the relation names its own local key.
|
|
132
|
+
if (collection) {
|
|
133
|
+
const relation = resolveCollectionRelations(collection)[orderBy];
|
|
134
|
+
if (relation?.kind === "belongsTo") {
|
|
135
|
+
const foreignKey = columnAt(relation.localKey);
|
|
136
|
+
if (foreignKey) return foreignKey;
|
|
78
137
|
}
|
|
79
138
|
}
|
|
80
|
-
|
|
139
|
+
|
|
140
|
+
// No collection in hand — the two shapes an owning relation's key takes
|
|
141
|
+
// by default (e.g. `project` → `project_id`, `userProfile` →
|
|
142
|
+
// `user_profile_id`).
|
|
143
|
+
for (const guess of [`${orderBy}_id`, generateForeignKeyName(orderBy)]) {
|
|
144
|
+
const foreignKey = columnAt(guess);
|
|
145
|
+
if (foreignKey) return foreignKey;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return undefined;
|
|
81
149
|
}
|
|
82
150
|
|
|
83
151
|
/**
|
|
@@ -85,7 +153,7 @@ export class FetchService {
|
|
|
85
153
|
* Converts collection relations to a Drizzle-compatible `with` object.
|
|
86
154
|
*
|
|
87
155
|
* When `include` is provided, only those relations are loaded.
|
|
88
|
-
* When `include` is absent, ALL relations are loaded (
|
|
156
|
+
* When `include` is absent, ALL relations are loaded (the admin path).
|
|
89
157
|
*
|
|
90
158
|
* Automatically detects many-to-many junction tables and nests
|
|
91
159
|
* the target relation so actual row data is returned.
|
|
@@ -311,7 +379,7 @@ export class FetchService {
|
|
|
311
379
|
}
|
|
312
380
|
|
|
313
381
|
if (options.logical) {
|
|
314
|
-
const logicalCondition = DrizzleConditionBuilder.buildLogicalConditions(options.logical, table, collectionPath);
|
|
382
|
+
const logicalCondition = DrizzleConditionBuilder.buildLogicalConditions(options.logical, table, collectionPath, this.filterContext(collectionPath, table));
|
|
315
383
|
if (logicalCondition) allConditions.push(logicalCondition);
|
|
316
384
|
}
|
|
317
385
|
|
|
@@ -656,7 +724,7 @@ _distance: vectorMeta.distanceSelect }).from(table).$dynamic()
|
|
|
656
724
|
}
|
|
657
725
|
|
|
658
726
|
if (options.logical) {
|
|
659
|
-
const logicalCondition = DrizzleConditionBuilder.buildLogicalConditions(options.logical, table, collectionPath);
|
|
727
|
+
const logicalCondition = DrizzleConditionBuilder.buildLogicalConditions(options.logical, table, collectionPath, this.filterContext(collectionPath, table));
|
|
660
728
|
if (logicalCondition) allConditions.push(logicalCondition);
|
|
661
729
|
}
|
|
662
730
|
|
|
@@ -14,7 +14,9 @@ import { PostgresCollectionRegistry } from "../collections/PostgresCollectionReg
|
|
|
14
14
|
*
|
|
15
15
|
* - `"ref"` — a `{ id, path, __type: "relation" }` reference carrying the
|
|
16
16
|
* target's values. This is what the admin renders.
|
|
17
|
-
* - `"inline"` — the target's own columns, flat. This is what REST serves
|
|
17
|
+
* - `"inline"` — the target's own columns, flat. This is what REST serves, and
|
|
18
|
+
* — since the in-process SDK reads through the same pipeline — what
|
|
19
|
+
* `rebase.data` / `context.data` serve too. A developer never sees a ref.
|
|
18
20
|
*
|
|
19
21
|
* They used to be two functions that happened to agree, and the agreement was
|
|
20
22
|
* not enforced by anything: the row-identity bug had to be fixed five times
|