@palbase/backend 24.3.0 → 25.0.1
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/bin/palbase-backend.cjs +103 -60
- package/dist/bin/palbase-backend.cjs.map +1 -1
- package/dist/bin/palbase-backend.js +17 -13
- package/dist/bin/palbase-backend.js.map +1 -1
- package/dist/{chunk-EIXCY4SS.js → chunk-34I4GB7D.js} +82 -49
- package/dist/chunk-34I4GB7D.js.map +1 -0
- package/dist/{chunk-UWSYTUGM.js → chunk-35PNTIRN.js} +48 -1
- package/dist/chunk-35PNTIRN.js.map +1 -0
- package/dist/chunk-HBOJLP2Z.js +840 -0
- package/dist/chunk-HBOJLP2Z.js.map +1 -0
- package/dist/{chunk-7Z6MGMXQ.js → chunk-XJ2RSHEU.js} +11 -5
- package/dist/chunk-XJ2RSHEU.js.map +1 -0
- package/dist/{chunk-ERDL5VAE.js → chunk-YOY5DFQS.js} +2 -2
- package/dist/db/env.cjs.map +1 -1
- package/dist/db/env.d.cts +31 -14
- package/dist/db/env.d.ts +31 -14
- package/dist/db/index.cjs +226 -111
- package/dist/db/index.cjs.map +1 -1
- package/dist/db/index.d.cts +1 -1
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.js +11 -1
- package/dist/engine/index.cjs +89 -50
- package/dist/engine/index.cjs.map +1 -1
- package/dist/engine/index.d.cts +2 -2
- package/dist/engine/index.d.ts +2 -2
- package/dist/engine/index.js +3 -3
- package/dist/{index-BTMYod_l.d.ts → index-B4CcpqLb.d.ts} +224 -75
- package/dist/{index-DEneI8Mn.d.ts → index-B8v6hVyU.d.ts} +5 -2
- package/dist/{index-BLAbr9ZH.d.cts → index-DmVyY6N7.d.cts} +224 -75
- package/dist/{index-C-ALG22n.d.cts → index-VsjBQ4Kw.d.cts} +5 -2
- package/dist/index.cjs +580 -301
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +125 -22
- package/dist/index.d.ts +125 -22
- package/dist/index.js +173 -217
- package/dist/index.js.map +1 -1
- package/dist/openapi/index.cjs +100 -36
- package/dist/openapi/index.cjs.map +1 -1
- package/dist/openapi/index.js +59 -2
- package/dist/openapi/index.js.map +1 -1
- package/docs/README.md +64 -31
- package/docs/endpoints.md +25 -28
- package/docs/llms-full.txt +430 -153
- package/docs/schema.md +303 -91
- package/docs/services.md +39 -4
- package/package.json +1 -1
- package/template/AGENTS.md +119 -314
- package/template/CLAUDE.md +13 -0
- package/template/controllers/notes.controller.ts +6 -13
- package/template/db/public.ts +38 -0
- package/template/models/notes/create.ts +38 -0
- package/template/package.json +6 -3
- package/template/services/note.service.test.ts +45 -0
- package/template/services/note.service.ts +2 -2
- package/dist/chunk-7Z6MGMXQ.js.map +0 -1
- package/dist/chunk-D5CQES25.js +0 -556
- package/dist/chunk-D5CQES25.js.map +0 -1
- package/dist/chunk-EIXCY4SS.js.map +0 -1
- package/dist/chunk-UWSYTUGM.js.map +0 -1
- package/template/db/schema.ts +0 -35
- /package/dist/{chunk-ERDL5VAE.js.map → chunk-YOY5DFQS.js.map} +0 -0
package/dist/chunk-D5CQES25.js
DELETED
|
@@ -1,556 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
TxPlanBuilder,
|
|
3
|
-
runTxPlan
|
|
4
|
-
} from "./chunk-P2Q27SGP.js";
|
|
5
|
-
|
|
6
|
-
// src/db/policy.ts
|
|
7
|
-
function quote(name) {
|
|
8
|
-
return `"${name.replace(/"/g, '""')}"`;
|
|
9
|
-
}
|
|
10
|
-
var PolicyBuilder = class {
|
|
11
|
-
_def;
|
|
12
|
-
constructor(name) {
|
|
13
|
-
this._def = {
|
|
14
|
-
name,
|
|
15
|
-
command: "all",
|
|
16
|
-
roles: ["authenticated"],
|
|
17
|
-
using: null,
|
|
18
|
-
withCheck: null,
|
|
19
|
-
permissive: true
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
/** Restrict the policy to a single SQL command (default `"all"`). */
|
|
23
|
-
for(command) {
|
|
24
|
-
this._def.command = command;
|
|
25
|
-
return this;
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Set the DB roles the policy applies to (the `TO` clause), replacing any
|
|
29
|
-
* previously-set roles. Call with no arguments to target PUBLIC (all roles).
|
|
30
|
-
*
|
|
31
|
-
* @example
|
|
32
|
-
* policy("p").to("authenticated")
|
|
33
|
-
* policy("p").to("authenticated", "service_role")
|
|
34
|
-
* policy("p").to() // PUBLIC
|
|
35
|
-
*/
|
|
36
|
-
to(...roles) {
|
|
37
|
-
this._def.roles = roles;
|
|
38
|
-
return this;
|
|
39
|
-
}
|
|
40
|
-
/** Set the `USING (...)` row-visibility expression (raw SQL). */
|
|
41
|
-
using(sqlExpr) {
|
|
42
|
-
this._def.using = sqlExpr;
|
|
43
|
-
return this;
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* "Rows of THIS table whose owner the caller is a member of" — the membership
|
|
47
|
-
* pattern, written so it cannot recurse.
|
|
48
|
-
*
|
|
49
|
-
* THE TRAP IT EXISTS FOR. Written by hand, membership policies point at each
|
|
50
|
-
* other: `channels` is visible to members, so its policy reads
|
|
51
|
-
* `channel_members`; `channel_members` is visible to members, so its policy
|
|
52
|
-
* reads `channels`. Postgres refuses the pair at query time with `infinite
|
|
53
|
-
* recursion detected in policy for relation ...`, and the error names the
|
|
54
|
-
* relation but not the cycle. The way out is asymmetry — the MEMBERSHIP table
|
|
55
|
-
* is protected by `user_id = auth.uid()` and nothing else, and every other
|
|
56
|
-
* table subqueries INTO it. That shape was in the platform's own schema and
|
|
57
|
-
* written down nowhere; a customer recovered it by reading that schema.
|
|
58
|
-
*
|
|
59
|
-
* `(select auth.uid())` rather than a bare call: the scalar subquery is
|
|
60
|
-
* evaluated ONCE per statement instead of per row.
|
|
61
|
-
*
|
|
62
|
-
* @example
|
|
63
|
-
* // channels: visible to members. The membership table gets the simple one.
|
|
64
|
-
* policy("member_read").for("select").to("authenticated")
|
|
65
|
-
* .memberOf("channel_members", "channel_id")
|
|
66
|
-
* // → id IN (SELECT "channel_id" FROM "channel_members"
|
|
67
|
-
* // WHERE "user_id" = (select auth.uid()))
|
|
68
|
-
*/
|
|
69
|
-
memberOf(membershipTable, foreignKey, options = {}) {
|
|
70
|
-
if (this._def.using !== null) {
|
|
71
|
-
throw new Error(
|
|
72
|
-
`policy(${this._def.name}).memberOf(): this policy already has a using() expression. Write one or the other \u2014 memberOf IS the using expression.`
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
const column = options.column ?? "id";
|
|
76
|
-
const userColumn = options.userColumn ?? "user_id";
|
|
77
|
-
this._def.using = `${quote(column)} IN (SELECT ${quote(foreignKey)} FROM ${quote(membershipTable)} WHERE ${quote(userColumn)} = (select auth.uid()))`;
|
|
78
|
-
return this;
|
|
79
|
-
}
|
|
80
|
-
/** Set the `WITH CHECK (...)` write-validation expression (raw SQL). */
|
|
81
|
-
withCheck(sqlExpr) {
|
|
82
|
-
this._def.withCheck = sqlExpr;
|
|
83
|
-
return this;
|
|
84
|
-
}
|
|
85
|
-
/** Set the policy mode: `"permissive"` (default, OR-combined) or
|
|
86
|
-
* `"restrictive"` (AND-combined). */
|
|
87
|
-
as(mode) {
|
|
88
|
-
this._def.permissive = mode === "permissive";
|
|
89
|
-
return this;
|
|
90
|
-
}
|
|
91
|
-
};
|
|
92
|
-
function policy(name) {
|
|
93
|
-
return new PolicyBuilder(name);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// src/db/schema.ts
|
|
97
|
-
function toPolicyDef(p) {
|
|
98
|
-
return p instanceof PolicyBuilder ? p._def : p;
|
|
99
|
-
}
|
|
100
|
-
function defineSchema(input) {
|
|
101
|
-
const tables = {};
|
|
102
|
-
for (const name of Object.keys(input.tables)) {
|
|
103
|
-
const table = input.tables[name];
|
|
104
|
-
if (table === void 0) continue;
|
|
105
|
-
const policies = (table.policies ?? []).map(toPolicyDef);
|
|
106
|
-
const rls = policies.length > 0 || table.rls !== false;
|
|
107
|
-
const tableDef = {
|
|
108
|
-
name,
|
|
109
|
-
columns: table.columns,
|
|
110
|
-
rls,
|
|
111
|
-
policies
|
|
112
|
-
};
|
|
113
|
-
if (table.primaryKey !== void 0) tableDef.primaryKey = table.primaryKey;
|
|
114
|
-
if (table.unique !== void 0) tableDef.unique = table.unique;
|
|
115
|
-
if (table.raw !== void 0 && table.raw.length > 0) tableDef.raw = table.raw.slice();
|
|
116
|
-
if (table.checks !== void 0 && table.checks.length > 0) tableDef.checks = table.checks.slice();
|
|
117
|
-
if (table.indexes !== void 0 && table.indexes.length > 0) tableDef.indexes = table.indexes.slice();
|
|
118
|
-
if (table.search !== void 0) {
|
|
119
|
-
const s = table.search;
|
|
120
|
-
if (s.from !== void 0) {
|
|
121
|
-
if (s.vector !== void 0) {
|
|
122
|
-
throw new Error(
|
|
123
|
-
`table ${String(name)}: search beyan\u0131nda tek bi\xE7im kullan\u0131n \u2014 'from' (yeni) ile 'vector' (eski) birlikte olamaz`
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
if (s.from.length === 0) {
|
|
127
|
-
throw new Error(`table ${String(name)}: search.from bo\u015F olamaz`);
|
|
128
|
-
}
|
|
129
|
-
if (s.model === void 0) {
|
|
130
|
-
throw new Error(
|
|
131
|
-
`table ${String(name)}: search.from model'siz anlams\u0131z \u2014 auto-embed i\xE7in model verin (BYO i\xE7in eski 'vector' bi\xE7imini kullan\u0131n)`
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
if (Array.isArray(s.text) && s.text.length === 0) {
|
|
135
|
-
throw new Error(`table ${String(name)}: search.text bo\u015F dizi olamaz \u2014 FTS istemiyorsan text: false yaz\u0131n`);
|
|
136
|
-
}
|
|
137
|
-
} else {
|
|
138
|
-
if (typeof s.text === "boolean") {
|
|
139
|
-
throw new Error(`table ${String(name)}: text:${String(s.text)} yaln\u0131z yeni bi\xE7imde ('from' ile) ge\xE7erli`);
|
|
140
|
-
}
|
|
141
|
-
if (s.text === void 0 && s.vector === void 0) {
|
|
142
|
-
throw new Error(
|
|
143
|
-
`table ${String(name)}: search beyan\u0131 bo\u015F \u2014 en az bir kol (text ya da vector) verin, yoksa alan\u0131 hi\xE7 yazmay\u0131n`
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
if (s.text !== void 0 && s.text.length === 0) {
|
|
147
|
-
throw new Error(`table ${String(name)}: search.text bo\u015F olamaz \u2014 FTS kolu istemiyorsan alan\u0131 hi\xE7 yazma`);
|
|
148
|
-
}
|
|
149
|
-
const legs = s.vector === void 0 ? [] : Array.isArray(s.vector) ? s.vector : [s.vector];
|
|
150
|
-
for (const leg of legs) {
|
|
151
|
-
if (leg.model !== void 0 && (leg.from === void 0 || leg.from.length === 0)) {
|
|
152
|
-
throw new Error(`table ${String(name)}: search.vector.model beyan edildi ama 'from' yok \u2014 embed kayna\u011F\u0131 kolonlar zorunlu (C-2)`);
|
|
153
|
-
}
|
|
154
|
-
if (leg.model === void 0 && leg.from !== void 0) {
|
|
155
|
-
throw new Error(`table ${String(name)}: search.vector.from model'siz anlams\u0131z \u2014 auto-embed i\xE7in model verin`);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
tableDef.search = s;
|
|
160
|
-
}
|
|
161
|
-
if (table.memory !== void 0) {
|
|
162
|
-
if (table.memory.from.length === 0) {
|
|
163
|
-
throw new Error(`table ${String(name)}: memory.from bo\u015F olamaz`);
|
|
164
|
-
}
|
|
165
|
-
tableDef.memory = table.memory;
|
|
166
|
-
}
|
|
167
|
-
tables[name] = tableDef;
|
|
168
|
-
}
|
|
169
|
-
for (const [name, def] of Object.entries(tables)) {
|
|
170
|
-
const m = def.memory;
|
|
171
|
-
if (m === void 0) continue;
|
|
172
|
-
const target = Object.values(tables).find((t) => t.name === m.into);
|
|
173
|
-
if (target === void 0) {
|
|
174
|
-
throw new Error(`table ${name}: memory.into "${m.into}" \u015Femada declared de\u011Fil`);
|
|
175
|
-
}
|
|
176
|
-
const subject = m.subject ?? "owner";
|
|
177
|
-
if (!(subject in def.columns)) {
|
|
178
|
-
throw new Error(`table ${name}: memory.subject "${subject}" kolonu kaynak tabloda yok`);
|
|
179
|
-
}
|
|
180
|
-
if (!(subject in target.columns)) {
|
|
181
|
-
throw new Error(`table ${name}: memory.subject "${subject}" kolonu hedef "${m.into}" tablosunda yok`);
|
|
182
|
-
}
|
|
183
|
-
const factCol = target.columns["fact"];
|
|
184
|
-
const factDef = factCol !== void 0 && "_def" in factCol ? factCol._def : factCol;
|
|
185
|
-
if (factDef === void 0 || factDef.type !== "text") {
|
|
186
|
-
throw new Error(`table ${name}: memory.into "${m.into}" tablosunda "fact" (text) kolonu zorunlu`);
|
|
187
|
-
}
|
|
188
|
-
for (const c of m.from) {
|
|
189
|
-
if (!(c in def.columns)) {
|
|
190
|
-
throw new Error(`table ${name}: memory.from kolonu "${c}" kaynak tabloda yok`);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
const extensions = [...new Set(input.extensions ?? [])];
|
|
195
|
-
return { tables, extensions };
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// src/db/extensions.ts
|
|
199
|
-
var PALBASE_EXTENSIONS = [
|
|
200
|
-
// Search & text
|
|
201
|
-
"vector",
|
|
202
|
-
// pgvector: AI embeddings + vector similarity search (semantic search / RAG).
|
|
203
|
-
// NB: the Postgres extension is named "vector", not "pgvector" — declare "vector".
|
|
204
|
-
"pg_trgm",
|
|
205
|
-
// trigram fuzzy / typo-tolerant text search
|
|
206
|
-
"unaccent",
|
|
207
|
-
// accent-insensitive text search
|
|
208
|
-
"citext",
|
|
209
|
-
// case-insensitive text type
|
|
210
|
-
// Geospatial / location
|
|
211
|
-
"cube",
|
|
212
|
-
// multi-dimensional cubes (dependency of earthdistance)
|
|
213
|
-
"earthdistance",
|
|
214
|
-
// great-circle distance (needs cube)
|
|
215
|
-
// Data types & structures
|
|
216
|
-
"hstore",
|
|
217
|
-
// key/value pairs in a single column
|
|
218
|
-
"ltree",
|
|
219
|
-
// hierarchical tree-structured labels
|
|
220
|
-
// Indexing & constraints
|
|
221
|
-
"btree_gist",
|
|
222
|
-
// GiST operator classes for scalar types — needed for EXCLUDE
|
|
223
|
-
// constraints that mix "=" with a range/&& overlap (e.g. no-double-booking).
|
|
224
|
-
// Scheduling
|
|
225
|
-
// Crypto / ids (also installed by default; listable for explicitness)
|
|
226
|
-
"pgcrypto",
|
|
227
|
-
// cryptographic functions (hashing, encryption)
|
|
228
|
-
"uuid-ossp"
|
|
229
|
-
// UUID generation functions
|
|
230
|
-
];
|
|
231
|
-
var EXTENSION_DEPENDENCIES = {
|
|
232
|
-
earthdistance: ["cube"]
|
|
233
|
-
};
|
|
234
|
-
function isPalbaseExtension(name) {
|
|
235
|
-
return PALBASE_EXTENSIONS.includes(name);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// src/db/columns.ts
|
|
239
|
-
function refuseOnVector(def, modifier) {
|
|
240
|
-
if (def.type === "vector") {
|
|
241
|
-
throw new Error(`vector column: .${modifier}() is not supported (FR-002 \u2014 allowed: nullable()/notNull())`);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
var ColumnBuilder = class _ColumnBuilder {
|
|
245
|
-
_def;
|
|
246
|
-
constructor(type, existingDef) {
|
|
247
|
-
this._def = existingDef ?? {
|
|
248
|
-
type,
|
|
249
|
-
nullable: false,
|
|
250
|
-
primaryKey: false
|
|
251
|
-
};
|
|
252
|
-
}
|
|
253
|
-
/** Mark this column as the primary key. */
|
|
254
|
-
primaryKey() {
|
|
255
|
-
refuseOnVector(this._def, "primaryKey");
|
|
256
|
-
this._def.primaryKey = true;
|
|
257
|
-
return new _ColumnBuilder(this._def.type, this._def);
|
|
258
|
-
}
|
|
259
|
-
/** Mark this column as NOT NULL (default). */
|
|
260
|
-
notNull() {
|
|
261
|
-
this._def.nullable = false;
|
|
262
|
-
return new _ColumnBuilder(this._def.type, this._def);
|
|
263
|
-
}
|
|
264
|
-
/** Allow NULL values. */
|
|
265
|
-
nullable() {
|
|
266
|
-
this._def.nullable = true;
|
|
267
|
-
return new _ColumnBuilder(this._def.type, this._def);
|
|
268
|
-
}
|
|
269
|
-
/** Set a default value. */
|
|
270
|
-
default(value) {
|
|
271
|
-
refuseOnVector(this._def, "default");
|
|
272
|
-
this._def.defaultValue = value;
|
|
273
|
-
return new _ColumnBuilder(this._def.type, this._def);
|
|
274
|
-
}
|
|
275
|
-
/** UUID: generate a random default (gen_random_uuid()). */
|
|
276
|
-
defaultRandom() {
|
|
277
|
-
refuseOnVector(this._def, "defaultRandom");
|
|
278
|
-
this._def.defaultRandom = true;
|
|
279
|
-
return new _ColumnBuilder(this._def.type, this._def);
|
|
280
|
-
}
|
|
281
|
-
/** Timestamp: default to now(). */
|
|
282
|
-
defaultNow() {
|
|
283
|
-
refuseOnVector(this._def, "defaultNow");
|
|
284
|
-
this._def.defaultNow = true;
|
|
285
|
-
return new _ColumnBuilder(this._def.type, this._def);
|
|
286
|
-
}
|
|
287
|
-
/**
|
|
288
|
-
* The DATABASE assigns this column's value — a trigger, a rule, an identity.
|
|
289
|
-
*
|
|
290
|
-
* The column becomes optional on INSERT (the author has nothing to send) while
|
|
291
|
-
* the DDL stays free of a DEFAULT this schema would not honour. It is NOT
|
|
292
|
-
* `default()`: that declares a value the schema promises to write.
|
|
293
|
-
*
|
|
294
|
-
* Naming: deliberately not `generated()`. Postgres has GENERATED columns and
|
|
295
|
-
* they are a different thing; borrowing the word would send a reader — or a
|
|
296
|
-
* model writing a schema — to the wrong feature.
|
|
297
|
-
*/
|
|
298
|
-
dbAssigned() {
|
|
299
|
-
this._def.dbAssigned = true;
|
|
300
|
-
return new _ColumnBuilder(this._def.type, this._def);
|
|
301
|
-
}
|
|
302
|
-
/** Add a foreign key reference. */
|
|
303
|
-
/**
|
|
304
|
-
* Declares that this column used to be called `previous`.
|
|
305
|
-
*
|
|
306
|
-
* A schema diff sees one name gone and another present; it cannot know whether
|
|
307
|
-
* you renamed a column or dropped one and added another, and the two are very
|
|
308
|
-
* different — the second loses every value. Saying so here turns the plan into
|
|
309
|
-
* `ALTER TABLE … RENAME COLUMN` instead.
|
|
310
|
-
*
|
|
311
|
-
* Once the rename has been applied the annotation is inert (the old name is no
|
|
312
|
-
* longer there to rename), so it can be deleted at your leisure.
|
|
313
|
-
*/
|
|
314
|
-
renamedFrom(previous) {
|
|
315
|
-
this._def.renamedFrom = previous;
|
|
316
|
-
return this;
|
|
317
|
-
}
|
|
318
|
-
/**
|
|
319
|
-
* See {@link ColumnDef.ignored}.
|
|
320
|
-
*
|
|
321
|
-
* COPIES the def rather than mutating it. The constructor takes an existing
|
|
322
|
-
* def BY REFERENCE, so every builder derived from another shares one object —
|
|
323
|
-
* `const a = slug.unique()` leaves `a._def === slug._def`. An in-place
|
|
324
|
-
* `ignored = true` therefore marks every column sharing that def, including
|
|
325
|
-
* one another table actively reads, and the gate would let THAT column be
|
|
326
|
-
* dropped. Measured before this copy existed.
|
|
327
|
-
*
|
|
328
|
-
* The aliasing is older than this method and other fields leak through it too.
|
|
329
|
-
* The reason this one cannot wait: every other leak produces a VISIBLE schema
|
|
330
|
-
* difference — the plan shows it, the DDL shows it. This one is invisible by
|
|
331
|
-
* design (no DDL, no diff, no plan line), so its only effect is to disarm a
|
|
332
|
-
* safety gate in silence.
|
|
333
|
-
*/
|
|
334
|
-
ignored() {
|
|
335
|
-
return new _ColumnBuilder(this._def.type, {
|
|
336
|
-
...this._def,
|
|
337
|
-
ignored: true
|
|
338
|
-
});
|
|
339
|
-
}
|
|
340
|
-
references(table, column) {
|
|
341
|
-
refuseOnVector(this._def, "references");
|
|
342
|
-
this._def.references = { table, column };
|
|
343
|
-
return new _ColumnBuilder(this._def.type, this._def);
|
|
344
|
-
}
|
|
345
|
-
/**
|
|
346
|
-
* Add a real DB-level foreign key to the built-in auth users
|
|
347
|
-
* (`REFERENCES auth.users(id)`), so a column like `user_id` gets true
|
|
348
|
-
* database cascade/integrity instead of app-layer-only. Sugar for
|
|
349
|
-
* `.references("auth.users", "id")`.
|
|
350
|
-
*
|
|
351
|
-
* `auth.users` lives in the SAME tenant database (palauth-owned), so this is
|
|
352
|
-
* a genuine cross-schema integrity constraint scoped to THIS tenant's users.
|
|
353
|
-
* The referenced `auth.users.id` is `text` (palauth ids are `usr_<uuid>`), so
|
|
354
|
-
* the referencing column must be `text()` too.
|
|
355
|
-
*
|
|
356
|
-
* ON DELETE is REQUIRED here and may only be `cascade` or `set null`: an
|
|
357
|
-
* account-erasure request must never be blocked by a lingering FK, so
|
|
358
|
-
* `restrict` / `no action` are not accepted (they don't type-check). Example:
|
|
359
|
-
* `text().notNull().referencesAuthUser("cascade")`, or
|
|
360
|
-
* `text().nullable().referencesAuthUser("set null")`. The server
|
|
361
|
-
* (validateAuthUserFK) enforces this — and the remaining rules the type can't
|
|
362
|
-
* express (referencing column is text, `set null` needs a nullable column) —
|
|
363
|
-
* as the real boundary; this signature is the compile-time DX mirror.
|
|
364
|
-
*/
|
|
365
|
-
referencesAuthUser(onDelete) {
|
|
366
|
-
refuseOnVector(this._def, "referencesAuthUser");
|
|
367
|
-
this._def.references = { table: "auth.users", column: "id" };
|
|
368
|
-
this._def.onDeleteAction = onDelete;
|
|
369
|
-
return new _ColumnBuilder(this._def.type, this._def);
|
|
370
|
-
}
|
|
371
|
-
/**
|
|
372
|
-
* Add a real DB-level foreign key to the canonical, server-minted installation
|
|
373
|
-
* anchor (`REFERENCES auth.installations(id)`) — the app-scoped verified-device
|
|
374
|
-
* root (`ins_...`). Sugar for `.references("auth.installations", "id")`.
|
|
375
|
-
*
|
|
376
|
-
* An installation is an APP INSTALL, not a user: this FK is NOT user ownership.
|
|
377
|
-
* A user-owned row STILL needs its own `.referencesAuthUser(...)` FK so account
|
|
378
|
-
* erasure removes it — an installation reference alone does not tie a row to a
|
|
379
|
-
* user's deletion. Use this only for install-scoped state (device prefs, push
|
|
380
|
-
* routing, …), alongside a separate auth-user FK where the row is user-owned.
|
|
381
|
-
*
|
|
382
|
-
* `auth.installations` lives in the SAME tenant DB (palauth-owned); its `id` is
|
|
383
|
-
* `text` (`ins_<uuid>`), so the referencing column must be `text()` too. ON
|
|
384
|
-
* DELETE is REQUIRED and may only be `cascade` or `set null` (same allowed set
|
|
385
|
-
* as an auth-user FK): an installation revoke / orphan cleanup must never be
|
|
386
|
-
* blocked by a lingering FK. The server (validateAuthAnchorFK) is the real
|
|
387
|
-
* boundary; this signature is the compile-time DX mirror.
|
|
388
|
-
*/
|
|
389
|
-
referencesInstallation(onDelete) {
|
|
390
|
-
refuseOnVector(this._def, "referencesInstallation");
|
|
391
|
-
this._def.references = { table: "auth.installations", column: "id" };
|
|
392
|
-
this._def.onDeleteAction = onDelete;
|
|
393
|
-
return new _ColumnBuilder(this._def.type, this._def);
|
|
394
|
-
}
|
|
395
|
-
/** Set the ON DELETE action for a foreign key reference. */
|
|
396
|
-
onDelete(action) {
|
|
397
|
-
this._def.onDeleteAction = action;
|
|
398
|
-
return new _ColumnBuilder(this._def.type, this._def);
|
|
399
|
-
}
|
|
400
|
-
/** Add a single-column UNIQUE constraint. */
|
|
401
|
-
unique() {
|
|
402
|
-
refuseOnVector(this._def, "unique");
|
|
403
|
-
this._def.unique = true;
|
|
404
|
-
return new _ColumnBuilder(this._def.type, this._def);
|
|
405
|
-
}
|
|
406
|
-
/**
|
|
407
|
-
* Declare how this column's value is projected in and out of the process.
|
|
408
|
-
*
|
|
409
|
-
* The DDL does not move: `numeric` stays `numeric`, and the driver still hands
|
|
410
|
-
* back what Postgres sent. What changes is the type the row surface exposes —
|
|
411
|
-
* it becomes `Target`:
|
|
412
|
-
*
|
|
413
|
-
* amount: numeric().transform<number>({ fromDb: Number, toDb: String })
|
|
414
|
-
*
|
|
415
|
-
* `numeric` surfacing as `string` is CORRECT (a JS number cannot hold
|
|
416
|
-
* arbitrary precision), and that is exactly why this exists: application code
|
|
417
|
-
* that does arithmetic on the column otherwise rewrites the same
|
|
418
|
-
* `Number(row.amount)` / `String(x)` pair in every controller that touches it,
|
|
419
|
-
* and each rewrite is a place the two directions can drift apart.
|
|
420
|
-
*
|
|
421
|
-
* A transform is a PROJECTION, never a constraint: it lives only in this
|
|
422
|
-
* process, so it can neither validate nor migrate what is stored.
|
|
423
|
-
*/
|
|
424
|
-
transform(fns) {
|
|
425
|
-
this._def.transform = fns;
|
|
426
|
-
return new _ColumnBuilder(this._def.type, this._def);
|
|
427
|
-
}
|
|
428
|
-
};
|
|
429
|
-
function uuid() {
|
|
430
|
-
return new ColumnBuilder("uuid");
|
|
431
|
-
}
|
|
432
|
-
function text() {
|
|
433
|
-
return new ColumnBuilder("text");
|
|
434
|
-
}
|
|
435
|
-
function integer() {
|
|
436
|
-
return new ColumnBuilder("integer");
|
|
437
|
-
}
|
|
438
|
-
function bigint() {
|
|
439
|
-
return new ColumnBuilder("bigint");
|
|
440
|
-
}
|
|
441
|
-
function numeric() {
|
|
442
|
-
return new ColumnBuilder("numeric");
|
|
443
|
-
}
|
|
444
|
-
function boolean() {
|
|
445
|
-
return new ColumnBuilder("boolean");
|
|
446
|
-
}
|
|
447
|
-
function timestamp() {
|
|
448
|
-
return new ColumnBuilder("timestamp");
|
|
449
|
-
}
|
|
450
|
-
function jsonb() {
|
|
451
|
-
return new ColumnBuilder("jsonb");
|
|
452
|
-
}
|
|
453
|
-
function enumType(name, values) {
|
|
454
|
-
const builder = new ColumnBuilder("enum");
|
|
455
|
-
builder._def.enumName = name;
|
|
456
|
-
builder._def.enumValues = [...values];
|
|
457
|
-
return builder;
|
|
458
|
-
}
|
|
459
|
-
function vector(dimensions) {
|
|
460
|
-
if (!Number.isInteger(dimensions) || dimensions < 1 || dimensions > 2e3) {
|
|
461
|
-
throw new Error(`vector(): dimensions must be an integer in [1, 2000], got ${String(dimensions)}`);
|
|
462
|
-
}
|
|
463
|
-
const b = new ColumnBuilder("vector");
|
|
464
|
-
b._def.dimensions = dimensions;
|
|
465
|
-
return b;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// src/db/raw.ts
|
|
469
|
-
function raw(name, up, opts) {
|
|
470
|
-
return { name, up, ...opts?.down != null ? { down: opts.down } : {} };
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
// src/db/embedding.ts
|
|
474
|
-
var openai = {
|
|
475
|
-
embedding(model, opts) {
|
|
476
|
-
return {
|
|
477
|
-
provider: "openai",
|
|
478
|
-
model,
|
|
479
|
-
apiKeyName: opts?.apiKeyName ?? "OPENAI_API_KEY",
|
|
480
|
-
...opts?.dimensions !== void 0 ? { dimensions: opts.dimensions } : {},
|
|
481
|
-
...opts?.baseURL !== void 0 ? { baseURL: opts.baseURL } : {}
|
|
482
|
-
};
|
|
483
|
-
},
|
|
484
|
-
chat(model) {
|
|
485
|
-
return { provider: "openai", model };
|
|
486
|
-
}
|
|
487
|
-
};
|
|
488
|
-
|
|
489
|
-
// src/db/typed-db.ts
|
|
490
|
-
function makeTypedTable(name, raw2) {
|
|
491
|
-
return {
|
|
492
|
-
insert: (data) => raw2.insert(name, data),
|
|
493
|
-
upsert: (data, opts) => raw2.upsert(name, data, opts),
|
|
494
|
-
update: (id, data) => raw2.update(name, id, data),
|
|
495
|
-
delete: (id) => raw2.delete(name, id),
|
|
496
|
-
findById: (id) => raw2.findById(name, id),
|
|
497
|
-
findMany: (query, opts) => raw2.findMany(name, query, opts),
|
|
498
|
-
updateMany: (where, set) => raw2.updateMany(
|
|
499
|
-
name,
|
|
500
|
-
where,
|
|
501
|
-
set
|
|
502
|
-
),
|
|
503
|
-
deleteMany: (where) => raw2.deleteMany(name, where),
|
|
504
|
-
count: (where) => raw2.count(name, where)
|
|
505
|
-
};
|
|
506
|
-
}
|
|
507
|
-
function makeTypedDB(schema, raw2) {
|
|
508
|
-
const tables = {};
|
|
509
|
-
for (const key of Object.keys(schema.tables)) {
|
|
510
|
-
const tableDef = schema.tables[key];
|
|
511
|
-
if (tableDef !== void 0) {
|
|
512
|
-
tables[key] = makeTypedTable(tableDef.name, raw2);
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
const result = {
|
|
516
|
-
tables,
|
|
517
|
-
transaction(fn) {
|
|
518
|
-
const builder = new TxPlanBuilder();
|
|
519
|
-
const planTables = {};
|
|
520
|
-
for (const key of Object.keys(schema.tables)) {
|
|
521
|
-
const tableDef = schema.tables[key];
|
|
522
|
-
if (tableDef !== void 0) planTables[key] = builder.table(tableDef.name);
|
|
523
|
-
}
|
|
524
|
-
return runTxPlan(
|
|
525
|
-
raw2,
|
|
526
|
-
planTables,
|
|
527
|
-
builder,
|
|
528
|
-
fn
|
|
529
|
-
);
|
|
530
|
-
}
|
|
531
|
-
};
|
|
532
|
-
return result;
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
export {
|
|
536
|
-
PolicyBuilder,
|
|
537
|
-
policy,
|
|
538
|
-
defineSchema,
|
|
539
|
-
PALBASE_EXTENSIONS,
|
|
540
|
-
EXTENSION_DEPENDENCIES,
|
|
541
|
-
isPalbaseExtension,
|
|
542
|
-
uuid,
|
|
543
|
-
text,
|
|
544
|
-
integer,
|
|
545
|
-
bigint,
|
|
546
|
-
numeric,
|
|
547
|
-
boolean,
|
|
548
|
-
timestamp,
|
|
549
|
-
jsonb,
|
|
550
|
-
enumType,
|
|
551
|
-
vector,
|
|
552
|
-
raw,
|
|
553
|
-
openai,
|
|
554
|
-
makeTypedDB
|
|
555
|
-
};
|
|
556
|
-
//# sourceMappingURL=chunk-D5CQES25.js.map
|