@palbase/backend 10.2.0 → 11.0.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/chunk-7LAXRLPG.js +418 -0
- package/dist/chunk-7LAXRLPG.js.map +1 -0
- package/dist/{chunk-4WOQWFUP.js → chunk-LUV36KQU.js} +26 -11
- package/dist/{chunk-4WOQWFUP.js.map → chunk-LUV36KQU.js.map} +1 -1
- package/dist/{chunk-RGQUB66H.js → chunk-XATG7BRC.js} +22 -2
- package/dist/chunk-XATG7BRC.js.map +1 -0
- package/dist/db/index.cjs +438 -10
- package/dist/db/index.cjs.map +1 -1
- package/dist/db/index.d.cts +2 -2
- package/dist/db/index.d.ts +2 -2
- package/dist/db/index.js +13 -1
- package/dist/{endpoint-Cn3ICGTf.d.cts → endpoint-Ck4hER_7.d.cts} +397 -11
- package/dist/{endpoint-Cn3ICGTf.d.ts → endpoint-Ck4hER_7.d.ts} +397 -11
- package/dist/{index-Bt7UHkAM.d.cts → index-BJAf1uPC.d.cts} +64 -34
- package/dist/{index-V0ealeY-.d.ts → index-l7DhBDtn.d.ts} +64 -34
- package/dist/index.cjs +776 -108
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +203 -9
- package/dist/index.d.ts +203 -9
- package/dist/index.js +325 -94
- package/dist/index.js.map +1 -1
- package/dist/purchases/keys.cjs +19 -0
- package/dist/purchases/keys.cjs.map +1 -0
- package/dist/purchases/keys.d.cts +42 -0
- package/dist/purchases/keys.d.ts +42 -0
- package/dist/purchases/keys.js +1 -0
- package/dist/purchases/keys.js.map +1 -0
- package/dist/test/index.cjs +559 -13
- package/dist/test/index.cjs.map +1 -1
- package/dist/test/index.d.cts +1 -1
- package/dist/test/index.d.ts +1 -1
- package/dist/test/index.js +166 -13
- package/dist/test/index.js.map +1 -1
- package/docs/README.md +7 -6
- package/docs/database.md +106 -16
- package/docs/getting-started.md +14 -12
- package/docs/llms-full.txt +140 -45
- package/docs/migrations.md +8 -8
- package/docs/schema.md +6 -4
- package/package.json +11 -1
- package/dist/chunk-RGQUB66H.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -49,6 +49,7 @@ __export(src_exports, {
|
|
|
49
49
|
Patch: () => Patch,
|
|
50
50
|
PolicyBuilder: () => PolicyBuilder,
|
|
51
51
|
Post: () => Post,
|
|
52
|
+
Purchases: () => Purchases,
|
|
52
53
|
Put: () => Put,
|
|
53
54
|
Query: () => Query,
|
|
54
55
|
QueryParams: () => QueryParams,
|
|
@@ -57,12 +58,16 @@ __export(src_exports, {
|
|
|
57
58
|
Realtime: () => Realtime,
|
|
58
59
|
Req: () => Req,
|
|
59
60
|
RequestId: () => RequestId,
|
|
61
|
+
RequireEntitlement: () => RequireEntitlement,
|
|
60
62
|
Resource: () => Resource,
|
|
61
63
|
STORAGE_CONFIG_KIND: () => STORAGE_CONFIG_KIND,
|
|
64
|
+
Spend: () => Spend,
|
|
62
65
|
Storage: () => Storage,
|
|
63
66
|
TEST_USERS_CONFIG_KIND: () => TEST_USERS_CONFIG_KIND,
|
|
64
67
|
TooManyRequests: () => TooManyRequests,
|
|
65
68
|
TraceId: () => TraceId,
|
|
69
|
+
TxPlanError: () => TxPlanError,
|
|
70
|
+
TxRefError: () => TxRefError,
|
|
66
71
|
Unauthorized: () => Unauthorized,
|
|
67
72
|
Upload: () => Upload,
|
|
68
73
|
UploadedObject: () => UploadedObject,
|
|
@@ -79,6 +84,7 @@ __export(src_exports, {
|
|
|
79
84
|
boolean: () => boolean,
|
|
80
85
|
bucket: () => bucket,
|
|
81
86
|
buildProvider: () => buildProvider,
|
|
87
|
+
dec: () => dec,
|
|
82
88
|
defineEgress: () => defineEgress,
|
|
83
89
|
defineError: () => defineError,
|
|
84
90
|
defineFlags: () => defineFlags,
|
|
@@ -91,21 +97,26 @@ __export(src_exports, {
|
|
|
91
97
|
defineWebhook: () => defineWebhook,
|
|
92
98
|
defineWorker: () => defineWorker,
|
|
93
99
|
documents: () => documents,
|
|
100
|
+
entitlementFor: () => entitlementFor,
|
|
94
101
|
enumType: () => enumType,
|
|
95
102
|
flag: () => flag,
|
|
96
103
|
getErrorRegistry: () => getErrorRegistry,
|
|
97
104
|
getRoutes: () => getRoutes,
|
|
105
|
+
inc: () => inc,
|
|
98
106
|
integer: () => integer,
|
|
99
107
|
isPalbaseExtension: () => isPalbaseExtension,
|
|
100
108
|
jsonb: () => jsonb,
|
|
101
109
|
makeEnvDts: () => makeEnvDts,
|
|
110
|
+
makePurchasesDts: () => makePurchasesDts,
|
|
102
111
|
makeTypedDB: () => makeTypedDB,
|
|
112
|
+
now: () => now,
|
|
103
113
|
numeric: () => numeric,
|
|
104
114
|
parseFileSizeLimit: () => parseFileSizeLimit,
|
|
105
115
|
policy: () => policy,
|
|
106
116
|
raw: () => raw,
|
|
107
117
|
recordThrows: () => recordThrows,
|
|
108
118
|
reservedSecretKey: () => reservedSecretKey,
|
|
119
|
+
spendFor: () => spendFor,
|
|
109
120
|
storage: () => storage,
|
|
110
121
|
testUser: () => testUser,
|
|
111
122
|
text: () => text,
|
|
@@ -118,6 +129,416 @@ module.exports = __toCommonJS(src_exports);
|
|
|
118
129
|
|
|
119
130
|
// src/runtime.ts
|
|
120
131
|
var import_node_async_hooks = require("async_hooks");
|
|
132
|
+
|
|
133
|
+
// src/db/tx-plan.ts
|
|
134
|
+
var TxRefError = class extends Error {
|
|
135
|
+
constructor(message) {
|
|
136
|
+
super(message);
|
|
137
|
+
this.name = "TxRefError";
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
var TxPlanError = class extends Error {
|
|
141
|
+
constructor(message) {
|
|
142
|
+
super(message);
|
|
143
|
+
this.name = "TxPlanError";
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
var EXPR = /* @__PURE__ */ Symbol.for("palbase.tx.expr");
|
|
147
|
+
var REF = /* @__PURE__ */ Symbol.for("palbase.tx.ref");
|
|
148
|
+
var ROW = /* @__PURE__ */ Symbol.for("palbase.tx.row");
|
|
149
|
+
var ROWS = /* @__PURE__ */ Symbol.for("palbase.tx.rows");
|
|
150
|
+
var TRAPPED_PROPS = [
|
|
151
|
+
"then",
|
|
152
|
+
"valueOf",
|
|
153
|
+
"toString",
|
|
154
|
+
"toJSON",
|
|
155
|
+
Symbol.toPrimitive
|
|
156
|
+
];
|
|
157
|
+
function trap(prop, what, hint) {
|
|
158
|
+
const name = typeof prop === "symbol" ? prop.description ?? String(prop) : prop;
|
|
159
|
+
throw new TxRefError(
|
|
160
|
+
`${what} was used as a value (via \`${name}\`). Nothing in a transaction callback has run yet, so there is no value to read. ${hint}`
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
function now() {
|
|
164
|
+
return makeExpr({ fn: "now" });
|
|
165
|
+
}
|
|
166
|
+
function inc(by) {
|
|
167
|
+
assertFiniteNumber(by, "inc");
|
|
168
|
+
return makeExpr({ fn: "inc", by });
|
|
169
|
+
}
|
|
170
|
+
function dec(by) {
|
|
171
|
+
assertFiniteNumber(by, "dec");
|
|
172
|
+
return makeExpr({ fn: "dec", by });
|
|
173
|
+
}
|
|
174
|
+
function assertFiniteNumber(by, fn) {
|
|
175
|
+
if (typeof by !== "number" || !Number.isFinite(by)) {
|
|
176
|
+
throw new TxPlanError(`${fn}() needs a finite number, got ${String(by)}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function makeExpr(expr) {
|
|
180
|
+
return new Proxy(
|
|
181
|
+
{ [EXPR]: expr },
|
|
182
|
+
{
|
|
183
|
+
get(target, prop) {
|
|
184
|
+
if (prop === EXPR) return target[EXPR];
|
|
185
|
+
if (TRAPPED_PROPS.includes(prop)) {
|
|
186
|
+
trap(prop, "A plan expression", "Write it into an operation instead.");
|
|
187
|
+
}
|
|
188
|
+
return void 0;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
function makeRef(op, field) {
|
|
194
|
+
const target = { [REF]: { op, field } };
|
|
195
|
+
return new Proxy(target, {
|
|
196
|
+
get(t, prop) {
|
|
197
|
+
if (prop === REF) return t[REF];
|
|
198
|
+
if (TRAPPED_PROPS.includes(prop)) {
|
|
199
|
+
trap(
|
|
200
|
+
prop,
|
|
201
|
+
`\`${field}\` of a row this transaction has not written yet`,
|
|
202
|
+
"Pass it to another operation in the same plan, or return it from the callback and read it after `transaction()` resolves."
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
return void 0;
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
function makeRowHandle(op) {
|
|
210
|
+
const target = { [ROW]: op };
|
|
211
|
+
return new Proxy(target, {
|
|
212
|
+
get(t, prop) {
|
|
213
|
+
if (prop === ROW) return t[ROW];
|
|
214
|
+
if (TRAPPED_PROPS.includes(prop)) {
|
|
215
|
+
trap(
|
|
216
|
+
prop,
|
|
217
|
+
"A row this transaction has not written yet",
|
|
218
|
+
"Read one of its columns to reference it, or return the row from the callback and read it after `transaction()` resolves."
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
if (typeof prop === "symbol") return void 0;
|
|
222
|
+
return makeRef(op, prop);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
function refDescriptor(v) {
|
|
227
|
+
if (typeof v !== "object" || v === null) return null;
|
|
228
|
+
const d = v[REF];
|
|
229
|
+
return isRefDescriptor(d) ? d : null;
|
|
230
|
+
}
|
|
231
|
+
function isRefDescriptor(d) {
|
|
232
|
+
return typeof d === "object" && d !== null && typeof d.op === "number" && typeof d.field === "string";
|
|
233
|
+
}
|
|
234
|
+
function rowOpIndex(v) {
|
|
235
|
+
if (typeof v !== "object" || v === null) return null;
|
|
236
|
+
const op = v[ROW];
|
|
237
|
+
return typeof op === "number" ? op : null;
|
|
238
|
+
}
|
|
239
|
+
function exprOf(v) {
|
|
240
|
+
if (typeof v !== "object" || v === null) return null;
|
|
241
|
+
const e = v[EXPR];
|
|
242
|
+
return typeof e === "object" && e !== null ? e : null;
|
|
243
|
+
}
|
|
244
|
+
function isRowsHandle(v) {
|
|
245
|
+
return typeof v === "object" && v !== null && v[ROWS] !== void 0;
|
|
246
|
+
}
|
|
247
|
+
function encodeValue(value, column, allowColumnExpr) {
|
|
248
|
+
const ref = refDescriptor(value);
|
|
249
|
+
if (ref) return { $ref: { op: ref.op, field: ref.field } };
|
|
250
|
+
const expr = exprOf(value);
|
|
251
|
+
if (expr) {
|
|
252
|
+
if (expr.fn !== "now" && !allowColumnExpr) {
|
|
253
|
+
throw new TxPlanError(
|
|
254
|
+
`\`${column}\`: ${expr.fn}() reads the column's current value, so it is only valid in updateWhere(where, set).`
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
return { $expr: expr };
|
|
258
|
+
}
|
|
259
|
+
if (rowOpIndex(value) !== null) {
|
|
260
|
+
throw new TxPlanError(
|
|
261
|
+
`\`${column}\`: a row handle is not a value. Read the column you meant (e.g. \`row.id\`).`
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
if (isRowsHandle(value)) {
|
|
265
|
+
throw new TxPlanError(
|
|
266
|
+
`\`${column}\`: an operation result is not a value. Declare an expectation first (\`.expectOne(err)\`) and read a column from the row.`
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
assertNoNestedHandles(value, column);
|
|
270
|
+
return value;
|
|
271
|
+
}
|
|
272
|
+
function assertNoNestedHandles(value, column) {
|
|
273
|
+
if (typeof value !== "object" || value === null) return;
|
|
274
|
+
if (value instanceof Date) return;
|
|
275
|
+
if (refDescriptor(value) || exprOf(value) || rowOpIndex(value) !== null || isRowsHandle(value)) {
|
|
276
|
+
throw new TxPlanError(
|
|
277
|
+
`\`${column}\`: a plan handle is nested inside a value. The server would store it as literal JSON, not resolve it. Put the reference directly in the column.`
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
if (Array.isArray(value)) {
|
|
281
|
+
for (const item of value) assertNoNestedHandles(item, column);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
for (const item of Object.values(value)) {
|
|
285
|
+
assertNoNestedHandles(item, column);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
function encodeMap(map, allowColumnExpr) {
|
|
289
|
+
const out = {};
|
|
290
|
+
for (const key of Object.keys(map).sort()) {
|
|
291
|
+
const value = map[key];
|
|
292
|
+
if (value === void 0) continue;
|
|
293
|
+
out[key] = encodeValue(value, key, allowColumnExpr);
|
|
294
|
+
}
|
|
295
|
+
return out;
|
|
296
|
+
}
|
|
297
|
+
var SKIPPED_OP = -1;
|
|
298
|
+
var TxRowsImpl = class {
|
|
299
|
+
constructor(builder, opIndex, what) {
|
|
300
|
+
this.builder = builder;
|
|
301
|
+
this.opIndex = opIndex;
|
|
302
|
+
this.what = what;
|
|
303
|
+
}
|
|
304
|
+
builder;
|
|
305
|
+
opIndex;
|
|
306
|
+
what;
|
|
307
|
+
// Present so `isRowsHandle` recognises the object; never read for its value.
|
|
308
|
+
[ROWS] = true;
|
|
309
|
+
guarded = false;
|
|
310
|
+
// The type-level `await` guard made real: TS rejects `await rows` at compile
|
|
311
|
+
// time, and reaching this means someone called `.then(...)` by hand.
|
|
312
|
+
then() {
|
|
313
|
+
throw new TxRefError(
|
|
314
|
+
`${this.what} cannot be awaited: a transaction callback builds a plan, it does not run statements. Remove the \`await\`.`
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
expectOne(error) {
|
|
318
|
+
this.declareGuard("one", 1, error);
|
|
319
|
+
if (this.opIndex === SKIPPED_OP) throw error;
|
|
320
|
+
return makeRowHandle(this.opIndex);
|
|
321
|
+
}
|
|
322
|
+
expectNone(error) {
|
|
323
|
+
this.declareGuard("none", 0, error);
|
|
324
|
+
}
|
|
325
|
+
expectAtLeast(n, error) {
|
|
326
|
+
assertGuardCount(n, "expectAtLeast");
|
|
327
|
+
this.declareGuard("atLeast", n, error);
|
|
328
|
+
if (this.opIndex === SKIPPED_OP && n > 0) throw error;
|
|
329
|
+
}
|
|
330
|
+
expectAtMost(n, error) {
|
|
331
|
+
assertGuardCount(n, "expectAtMost");
|
|
332
|
+
this.declareGuard("atMost", n, error);
|
|
333
|
+
}
|
|
334
|
+
declareGuard(kind, n, error) {
|
|
335
|
+
if (!(error instanceof Error)) {
|
|
336
|
+
throw new TxPlanError(
|
|
337
|
+
`${this.what}: an expectation needs the Error to throw when it does not hold (e.g. \`.expect\u2026(new Conflict("already accepted"))\`).`
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
if (this.guarded) {
|
|
341
|
+
throw new TxPlanError(
|
|
342
|
+
`${this.what} already has an expectation. One operation carries one expectation; declare the second one on its own operation.`
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
this.guarded = true;
|
|
346
|
+
if (this.opIndex === SKIPPED_OP) return;
|
|
347
|
+
this.builder.attachGuard(this.opIndex, kind, n, error);
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
function assertGuardCount(n, fn) {
|
|
351
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
352
|
+
throw new TxPlanError(`${fn}(n) needs a non-negative integer, got ${String(n)}`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
var MAX_OPS = 1e3;
|
|
356
|
+
var MAX_ROWS = 5e3;
|
|
357
|
+
var TxPlanBuilder = class {
|
|
358
|
+
ops = [];
|
|
359
|
+
/** Errors handed to expectations, indexed by the `slot` the server echoes. */
|
|
360
|
+
slots = [];
|
|
361
|
+
/** The table surface handed to the callback. Untyped here; the public
|
|
362
|
+
* `transaction()` signatures put the schema types on top. */
|
|
363
|
+
table(name) {
|
|
364
|
+
return {
|
|
365
|
+
insert: (values) => {
|
|
366
|
+
const encoded = encodeMap(values, false);
|
|
367
|
+
if (Object.keys(encoded).length === 0) {
|
|
368
|
+
throw new TxPlanError(`${name}.insert() needs at least one column`);
|
|
369
|
+
}
|
|
370
|
+
return this.push({ op: "insert", table: name, values: encoded }, `${name}.insert()`);
|
|
371
|
+
},
|
|
372
|
+
insertMany: (rows) => {
|
|
373
|
+
if (rows.length === 0) {
|
|
374
|
+
return new TxRowsImpl(this, SKIPPED_OP, `${name}.insertMany()`);
|
|
375
|
+
}
|
|
376
|
+
if (rows.length > MAX_ROWS) {
|
|
377
|
+
throw new TxPlanError(
|
|
378
|
+
`${name}.insertMany() has ${rows.length} rows; the limit is ${MAX_ROWS}. Split the write across requests.`
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
const encoded = rows.map((row) => encodeMap(row, false));
|
|
382
|
+
assertUniformRows(encoded, name);
|
|
383
|
+
return this.push({ op: "insertMany", table: name, rows: encoded }, `${name}.insertMany()`);
|
|
384
|
+
},
|
|
385
|
+
updateWhere: (where, set) => {
|
|
386
|
+
const encodedWhere = encodeMap(where, false);
|
|
387
|
+
const encodedSet = encodeMap(set, true);
|
|
388
|
+
if (Object.keys(encodedWhere).length === 0) {
|
|
389
|
+
throw new TxPlanError(
|
|
390
|
+
`${name}.updateWhere() needs a filter. An update with no filter rewrites the whole table.`
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
if (Object.keys(encodedSet).length === 0) {
|
|
394
|
+
throw new TxPlanError(`${name}.updateWhere() needs at least one column to set`);
|
|
395
|
+
}
|
|
396
|
+
return this.push(
|
|
397
|
+
{ op: "update", table: name, set: encodedSet, where: encodedWhere },
|
|
398
|
+
`${name}.updateWhere()`
|
|
399
|
+
);
|
|
400
|
+
},
|
|
401
|
+
deleteWhere: (where) => {
|
|
402
|
+
const encodedWhere = encodeMap(where, false);
|
|
403
|
+
if (Object.keys(encodedWhere).length === 0) {
|
|
404
|
+
throw new TxPlanError(
|
|
405
|
+
`${name}.deleteWhere() needs a filter. A delete with no filter empties the table.`
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
return this.push(
|
|
409
|
+
{ op: "delete", table: name, where: encodedWhere },
|
|
410
|
+
`${name}.deleteWhere()`
|
|
411
|
+
);
|
|
412
|
+
},
|
|
413
|
+
select: (where, options) => {
|
|
414
|
+
const op = { op: "select", table: name };
|
|
415
|
+
const encodedWhere = encodeMap(where ?? {}, false);
|
|
416
|
+
if (Object.keys(encodedWhere).length > 0) op.where = encodedWhere;
|
|
417
|
+
if (options?.limit !== void 0) {
|
|
418
|
+
if (!Number.isInteger(options.limit) || options.limit < 0) {
|
|
419
|
+
throw new TxPlanError(
|
|
420
|
+
`${name}.select(): limit needs a non-negative integer, got ${String(options.limit)}`
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
op.limit = options.limit;
|
|
424
|
+
}
|
|
425
|
+
if (options?.lock !== void 0) op.lock = options.lock;
|
|
426
|
+
return this.push(op, `${name}.select()`);
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
push(op, what) {
|
|
431
|
+
if (this.ops.length >= MAX_OPS) {
|
|
432
|
+
throw new TxPlanError(
|
|
433
|
+
`this transaction has ${MAX_OPS} operations, which is the limit. Use insertMany() for bulk writes, or split the work across requests.`
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
const index = this.ops.length;
|
|
437
|
+
this.ops.push(op);
|
|
438
|
+
return new TxRowsImpl(this, index, what);
|
|
439
|
+
}
|
|
440
|
+
/** Attach an expectation to an op and record its error in the slot table. */
|
|
441
|
+
attachGuard(opIndex, kind, n, error) {
|
|
442
|
+
const op = this.ops[opIndex];
|
|
443
|
+
if (!op) throw new TxPlanError(`internal: expectation on unknown operation ${opIndex}`);
|
|
444
|
+
const slot = this.slots.length;
|
|
445
|
+
this.slots.push(error);
|
|
446
|
+
op.guard = { kind, n, slot };
|
|
447
|
+
}
|
|
448
|
+
/** The serialisable plan. Empty when the callback described no writes. */
|
|
449
|
+
body() {
|
|
450
|
+
return { ops: this.ops };
|
|
451
|
+
}
|
|
452
|
+
/** The error the server's `slot` selects, or `null` when it names one this
|
|
453
|
+
* plan never declared (a server/client disagreement, not a tenant error). */
|
|
454
|
+
errorForSlot(slot) {
|
|
455
|
+
return this.slots[slot] ?? null;
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
function assertUniformRows(rows, table) {
|
|
459
|
+
const first = rows[0];
|
|
460
|
+
if (!first) return;
|
|
461
|
+
const want = Object.keys(first);
|
|
462
|
+
const wantKey = want.join(",");
|
|
463
|
+
for (let i = 1; i < rows.length; i++) {
|
|
464
|
+
const got = Object.keys(rows[i]);
|
|
465
|
+
if (got.join(",") !== wantKey) {
|
|
466
|
+
throw new TxPlanError(
|
|
467
|
+
`${table}.insertMany(): every row must set the same columns. Row 0 sets [${want.join(", ")}] but row ${i} sets [${got.join(", ")}]. (A property set to \`undefined\` counts as absent \u2014 use \`null\`.)`
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
function materializeResult(value, results) {
|
|
473
|
+
const ref = refDescriptor(value);
|
|
474
|
+
if (ref) {
|
|
475
|
+
const row = rowOf(results, ref.op, `\`${ref.field}\``);
|
|
476
|
+
if (!(ref.field in row)) {
|
|
477
|
+
throw new TxPlanError(
|
|
478
|
+
`the transaction's operation ${ref.op} returned no column \`${ref.field}\`.`
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
return row[ref.field];
|
|
482
|
+
}
|
|
483
|
+
const rowOp = rowOpIndex(value);
|
|
484
|
+
if (rowOp !== null) return rowOf(results, rowOp, "a row");
|
|
485
|
+
if (isRowsHandle(value)) {
|
|
486
|
+
throw new TxPlanError(
|
|
487
|
+
"an operation result cannot be returned from a transaction callback: its row count is not known until the plan runs. Declare an expectation (`.expectOne(err)`) and return the row, or a column of it."
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
if (Array.isArray(value)) return value.map((item) => materializeResult(item, results));
|
|
491
|
+
if (isPlainObject(value)) {
|
|
492
|
+
const out = {};
|
|
493
|
+
for (const [key, item] of Object.entries(value)) out[key] = materializeResult(item, results);
|
|
494
|
+
return out;
|
|
495
|
+
}
|
|
496
|
+
return value;
|
|
497
|
+
}
|
|
498
|
+
function rowOf(results, opIndex, what) {
|
|
499
|
+
const result = results[opIndex];
|
|
500
|
+
if (!result) {
|
|
501
|
+
throw new TxPlanError(
|
|
502
|
+
`the transaction returned no result for operation ${opIndex}, so ${what} cannot be read.`
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
const row = result.rows[0];
|
|
506
|
+
if (!row) {
|
|
507
|
+
throw new TxPlanError(
|
|
508
|
+
`the transaction's operation ${opIndex} returned no row, so ${what} cannot be read.`
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
return row;
|
|
512
|
+
}
|
|
513
|
+
function isPlainObject(value) {
|
|
514
|
+
if (typeof value !== "object" || value === null) return false;
|
|
515
|
+
const proto = Object.getPrototypeOf(value);
|
|
516
|
+
return proto === Object.prototype || proto === null;
|
|
517
|
+
}
|
|
518
|
+
async function runTxPlan(transport, tables, builder, fn) {
|
|
519
|
+
const returned = fn({ tables });
|
|
520
|
+
const body = builder.body();
|
|
521
|
+
if (body.ops.length === 0) {
|
|
522
|
+
return materializeResult(returned, []);
|
|
523
|
+
}
|
|
524
|
+
let response;
|
|
525
|
+
try {
|
|
526
|
+
response = await transport.txPlan(body);
|
|
527
|
+
} catch (err) {
|
|
528
|
+
throw translateRejection(err, builder);
|
|
529
|
+
}
|
|
530
|
+
return materializeResult(returned, response.results);
|
|
531
|
+
}
|
|
532
|
+
function translateRejection(err, builder) {
|
|
533
|
+
if (typeof err !== "object" || err === null) return err;
|
|
534
|
+
const rejection = err;
|
|
535
|
+
if (rejection.error_code !== "tx_guard_failed" || typeof rejection.slot !== "number") {
|
|
536
|
+
return err;
|
|
537
|
+
}
|
|
538
|
+
return builder.errorForSlot(rejection.slot) ?? err;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// src/runtime.ts
|
|
121
542
|
var __requestALS = new import_node_async_hooks.AsyncLocalStorage();
|
|
122
543
|
var runtime = null;
|
|
123
544
|
function __setRuntime(services) {
|
|
@@ -178,10 +599,23 @@ function makeTypedSurface(raw2) {
|
|
|
178
599
|
return Object.assign(ops, {
|
|
179
600
|
tables: makeTablesAccessor(() => raw2),
|
|
180
601
|
transaction(fn) {
|
|
181
|
-
|
|
602
|
+
const builder = new TxPlanBuilder();
|
|
603
|
+
return runTxPlan(raw2, makeTxTablesAccessor(builder), builder, fn);
|
|
182
604
|
}
|
|
183
605
|
});
|
|
184
606
|
}
|
|
607
|
+
function makeTxTablesAccessor(builder) {
|
|
608
|
+
const tablesProxy = new Proxy(
|
|
609
|
+
{},
|
|
610
|
+
{
|
|
611
|
+
get(_t, prop) {
|
|
612
|
+
if (typeof prop !== "string") return void 0;
|
|
613
|
+
return builder.table(prop);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
);
|
|
617
|
+
return tablesProxy;
|
|
618
|
+
}
|
|
185
619
|
var Database = Object.assign(makeTypedSurface(rawDatabase), {
|
|
186
620
|
/**
|
|
187
621
|
* Lazily resolve the runtime's service-role sibling on each call. We do NOT
|
|
@@ -200,6 +634,7 @@ var Cache = makeServiceProxy("Cache");
|
|
|
200
634
|
var Queue = makeServiceProxy("Queue");
|
|
201
635
|
var Log = makeServiceProxy("Log");
|
|
202
636
|
var Notifications = makeServiceProxy("Notifications");
|
|
637
|
+
var Purchases = makeServiceProxy("Purchases");
|
|
203
638
|
var rawFlags = makeServiceProxy("Flags");
|
|
204
639
|
var Flags = Object.assign(
|
|
205
640
|
{
|
|
@@ -230,6 +665,276 @@ var Flags = Object.assign(
|
|
|
230
665
|
);
|
|
231
666
|
var Realtime = makeServiceProxy("Realtime");
|
|
232
667
|
|
|
668
|
+
// src/errors.ts
|
|
669
|
+
var HttpError = class extends Error {
|
|
670
|
+
status;
|
|
671
|
+
error;
|
|
672
|
+
errorDescription;
|
|
673
|
+
data;
|
|
674
|
+
constructor(status, error, errorDescription, data) {
|
|
675
|
+
super(errorDescription);
|
|
676
|
+
this.name = "HttpError";
|
|
677
|
+
this.status = status;
|
|
678
|
+
this.error = error;
|
|
679
|
+
this.errorDescription = errorDescription;
|
|
680
|
+
if (data !== void 0) {
|
|
681
|
+
this.data = data;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Serialize to the standard Palbase error response format.
|
|
686
|
+
* The `requestId` is injected by the runtime layer from the request context.
|
|
687
|
+
* When called without arguments (e.g. JSON.stringify), request_id is omitted.
|
|
688
|
+
* When `data` is set, it is appended as a strict-superset field.
|
|
689
|
+
*/
|
|
690
|
+
toJSON(requestId) {
|
|
691
|
+
const result = {
|
|
692
|
+
error: this.error,
|
|
693
|
+
error_description: this.errorDescription,
|
|
694
|
+
status: this.status
|
|
695
|
+
};
|
|
696
|
+
if (requestId) {
|
|
697
|
+
result.request_id = requestId;
|
|
698
|
+
}
|
|
699
|
+
if (this.data !== void 0) {
|
|
700
|
+
result.data = this.data;
|
|
701
|
+
}
|
|
702
|
+
return result;
|
|
703
|
+
}
|
|
704
|
+
};
|
|
705
|
+
var PalError = class extends HttpError {
|
|
706
|
+
constructor(status, code, description, data) {
|
|
707
|
+
super(status, code, description, data);
|
|
708
|
+
this.name = "PalError";
|
|
709
|
+
}
|
|
710
|
+
};
|
|
711
|
+
var NamedHttpError = class extends HttpError {
|
|
712
|
+
constructor(status, defaultCode, name, message, code, data) {
|
|
713
|
+
super(status, code ?? defaultCode, message ?? defaultMessage(name), data);
|
|
714
|
+
this.name = name;
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
function defaultMessage(name) {
|
|
718
|
+
const spaced = name.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
|
|
719
|
+
return spaced.charAt(0).toUpperCase() + spaced.slice(1).toLowerCase();
|
|
720
|
+
}
|
|
721
|
+
var BadRequest = class extends NamedHttpError {
|
|
722
|
+
constructor(data, message) {
|
|
723
|
+
super(400, "bad_request", "BadRequest", message, void 0, data);
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
var Unauthorized = class extends NamedHttpError {
|
|
727
|
+
constructor(message, code, data) {
|
|
728
|
+
super(401, "unauthorized", "Unauthorized", message, code, data);
|
|
729
|
+
}
|
|
730
|
+
};
|
|
731
|
+
var Forbidden = class extends NamedHttpError {
|
|
732
|
+
constructor(message, code, data) {
|
|
733
|
+
super(403, "forbidden", "Forbidden", message, code, data);
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
var NotFound = class extends NamedHttpError {
|
|
737
|
+
constructor(message, code, data) {
|
|
738
|
+
super(404, "not_found", "NotFound", message, code, data);
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
var Conflict = class extends NamedHttpError {
|
|
742
|
+
constructor(message, code, data) {
|
|
743
|
+
super(409, "conflict", "Conflict", message, code, data);
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
var TooManyRequests = class extends NamedHttpError {
|
|
747
|
+
constructor(data, message) {
|
|
748
|
+
super(429, "too_many_requests", "TooManyRequests", message, void 0, data);
|
|
749
|
+
}
|
|
750
|
+
};
|
|
751
|
+
|
|
752
|
+
// src/purchases/errors.ts
|
|
753
|
+
function asPalstoreError(err) {
|
|
754
|
+
if (!(err instanceof Error)) return null;
|
|
755
|
+
const e = err;
|
|
756
|
+
if (typeof e.code !== "string" || typeof e.status !== "number") return null;
|
|
757
|
+
return { code: e.code, status: e.status, message: e.message };
|
|
758
|
+
}
|
|
759
|
+
function toHttpError(err) {
|
|
760
|
+
if (err instanceof HttpError) return err;
|
|
761
|
+
const p = asPalstoreError(err);
|
|
762
|
+
if (!p) return err;
|
|
763
|
+
switch (p.code) {
|
|
764
|
+
case "entitlement_required": {
|
|
765
|
+
const entitlement = err.entitlement ?? null;
|
|
766
|
+
return new PalError(403, "entitlement_required", p.message, { entitlement });
|
|
767
|
+
}
|
|
768
|
+
case "quota_exceeded":
|
|
769
|
+
case "credit_insufficient": {
|
|
770
|
+
const state = err.state;
|
|
771
|
+
return new PalError(429, p.code, p.message, state);
|
|
772
|
+
}
|
|
773
|
+
default:
|
|
774
|
+
return err;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// src/purchases/registry.ts
|
|
779
|
+
var ENTITLEMENTS = /* @__PURE__ */ Symbol.for("palbase.backend.purchases.entitlements");
|
|
780
|
+
var SPENDS = /* @__PURE__ */ Symbol.for("palbase.backend.purchases.spends");
|
|
781
|
+
function carrierOf(target) {
|
|
782
|
+
return typeof target === "function" ? target : target.constructor ?? target;
|
|
783
|
+
}
|
|
784
|
+
function own(carrier, slot) {
|
|
785
|
+
if (!Object.prototype.hasOwnProperty.call(carrier, slot)) {
|
|
786
|
+
carrier[slot] = {};
|
|
787
|
+
}
|
|
788
|
+
return carrier[slot];
|
|
789
|
+
}
|
|
790
|
+
function recordEntitlement(target, fnName, key) {
|
|
791
|
+
own(carrierOf(target), ENTITLEMENTS)[fnName] = key;
|
|
792
|
+
}
|
|
793
|
+
function recordSpend(target, fnName, meta) {
|
|
794
|
+
own(carrierOf(target), SPENDS)[fnName] = meta;
|
|
795
|
+
}
|
|
796
|
+
function entitlementFor(ctor, fnName) {
|
|
797
|
+
return carrierOf(ctor)[ENTITLEMENTS]?.[fnName];
|
|
798
|
+
}
|
|
799
|
+
function spendFor(ctor, fnName) {
|
|
800
|
+
return carrierOf(ctor)[SPENDS]?.[fnName];
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// src/purchases/subject.ts
|
|
804
|
+
var subjectByRequest = /* @__PURE__ */ new WeakMap();
|
|
805
|
+
function storeEnv() {
|
|
806
|
+
return process.env.PALSTORE_STORE_ENV === "sandbox" ? "sandbox" : "production";
|
|
807
|
+
}
|
|
808
|
+
async function currentSubjectId() {
|
|
809
|
+
const store = __requestALS.getStore();
|
|
810
|
+
if (!store) {
|
|
811
|
+
throw new Error(
|
|
812
|
+
"Purchases decorators used outside a request scope. @RequireEntitlement/@Spend resolve the subject from the request's authenticated user, so they only work inside an endpoint handler."
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
const memoized = subjectByRequest.get(store);
|
|
816
|
+
if (memoized) return memoized;
|
|
817
|
+
const userId = store.userId;
|
|
818
|
+
if (!userId) {
|
|
819
|
+
throw new Unauthorized(
|
|
820
|
+
"This endpoint requires an authenticated user: purchases entitlements and quota are per-user, and the subject is resolved from the request's identity."
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
const resolving = Purchases.resolveSubject({ userRef: userId, storeEnv: storeEnv() }).then(
|
|
824
|
+
(r) => r.subjectId
|
|
825
|
+
);
|
|
826
|
+
subjectByRequest.set(store, resolving);
|
|
827
|
+
resolving.catch(() => subjectByRequest.delete(store));
|
|
828
|
+
return resolving;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// src/purchases/decorators.ts
|
|
832
|
+
var WRAPPED = /* @__PURE__ */ Symbol.for("palbase.backend.purchases.wrapped");
|
|
833
|
+
function idempotencyKeyFor(key) {
|
|
834
|
+
const store = __requestALS.getStore();
|
|
835
|
+
const base = store?.idempotencyKey || store?.requestId;
|
|
836
|
+
if (!base) {
|
|
837
|
+
throw new Error(
|
|
838
|
+
"@Spend needs a request-scoped idempotency key, and the runtime supplied neither an Idempotency-Key header nor a request id. Spending without one would let a retried request charge the user twice."
|
|
839
|
+
);
|
|
840
|
+
}
|
|
841
|
+
return `${base}:${key}`;
|
|
842
|
+
}
|
|
843
|
+
function ensureWrapper(target, fnName, descriptor) {
|
|
844
|
+
const original = descriptor.value;
|
|
845
|
+
if (typeof original !== "function") {
|
|
846
|
+
throw new Error(
|
|
847
|
+
`@RequireEntitlement/@Spend can only decorate a method; "${fnName}" is not one.`
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
if (original[WRAPPED]) return;
|
|
851
|
+
const wrapper = async function(...args) {
|
|
852
|
+
const ctor = this.constructor;
|
|
853
|
+
const entitlement = entitlementFor(ctor, fnName);
|
|
854
|
+
const spend = spendFor(ctor, fnName);
|
|
855
|
+
let subjectId;
|
|
856
|
+
try {
|
|
857
|
+
subjectId = await currentSubjectId();
|
|
858
|
+
} catch (err) {
|
|
859
|
+
throw toHttpError(err);
|
|
860
|
+
}
|
|
861
|
+
if (entitlement !== void 0) {
|
|
862
|
+
try {
|
|
863
|
+
await Purchases.require(subjectId, entitlement);
|
|
864
|
+
} catch (err) {
|
|
865
|
+
throw toHttpError(err);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
if (spend === void 0) {
|
|
869
|
+
return original.apply(this, args);
|
|
870
|
+
}
|
|
871
|
+
let handlerFailure;
|
|
872
|
+
try {
|
|
873
|
+
return await Purchases.withSpend(
|
|
874
|
+
subjectId,
|
|
875
|
+
spend.key,
|
|
876
|
+
{ count: spend.count, idempotencyKey: idempotencyKeyFor(spend.key) },
|
|
877
|
+
async () => {
|
|
878
|
+
try {
|
|
879
|
+
return await original.apply(this, args);
|
|
880
|
+
} catch (err) {
|
|
881
|
+
handlerFailure = { err };
|
|
882
|
+
throw err;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
);
|
|
886
|
+
} catch (err) {
|
|
887
|
+
if (handlerFailure !== void 0 && handlerFailure.err === err) throw err;
|
|
888
|
+
throw toHttpError(err);
|
|
889
|
+
}
|
|
890
|
+
};
|
|
891
|
+
wrapper[WRAPPED] = true;
|
|
892
|
+
descriptor.value = wrapper;
|
|
893
|
+
}
|
|
894
|
+
function RequireEntitlement(key) {
|
|
895
|
+
return function(target, propertyKey, descriptor) {
|
|
896
|
+
const fnName = String(propertyKey);
|
|
897
|
+
recordEntitlement(target, fnName, key);
|
|
898
|
+
ensureWrapper(target, fnName, descriptor);
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
function Spend(key) {
|
|
902
|
+
return function(target, propertyKey, descriptor) {
|
|
903
|
+
const fnName = String(propertyKey);
|
|
904
|
+
recordSpend(target, fnName, { key, count: 1 });
|
|
905
|
+
ensureWrapper(target, fnName, descriptor);
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// src/purchases/keys-gen.ts
|
|
910
|
+
function liveKeys(entries) {
|
|
911
|
+
return Object.entries(entries ?? {}).filter(([, def]) => def?.removed !== true).map(([key]) => key).sort();
|
|
912
|
+
}
|
|
913
|
+
function memberName(key) {
|
|
914
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : JSON.stringify(key);
|
|
915
|
+
}
|
|
916
|
+
function members(keys) {
|
|
917
|
+
if (keys.length === 0) return "";
|
|
918
|
+
return `
|
|
919
|
+
${keys.map((k) => ` ${memberName(k)}: true;`).join("\n")}
|
|
920
|
+
`;
|
|
921
|
+
}
|
|
922
|
+
function makePurchasesDts(manifest) {
|
|
923
|
+
const entitlements = liveKeys(manifest.entitlements);
|
|
924
|
+
const limits = liveKeys({ ...manifest.limits, ...manifest.credits });
|
|
925
|
+
return `// palbase-purchases.d.ts \u2014 GENERATED by @palbase/backend. Do not edit.
|
|
926
|
+
// Source: the project's palstore catalog revision.
|
|
927
|
+
|
|
928
|
+
declare module "@palbase/backend/purchases" {
|
|
929
|
+
interface Entitlements {${members(entitlements)}}
|
|
930
|
+
|
|
931
|
+
interface Limits {${members(limits)}}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
export {};
|
|
935
|
+
`;
|
|
936
|
+
}
|
|
937
|
+
|
|
233
938
|
// src/db/policy.ts
|
|
234
939
|
var PolicyBuilder = class {
|
|
235
940
|
_def;
|
|
@@ -505,19 +1210,29 @@ function makeTypedTable(name, raw2) {
|
|
|
505
1210
|
};
|
|
506
1211
|
}
|
|
507
1212
|
function makeTypedDB(schema, raw2) {
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
tables[key] = makeTypedTable(tableDef.name, client);
|
|
514
|
-
}
|
|
1213
|
+
const tables = {};
|
|
1214
|
+
for (const key of Object.keys(schema.tables)) {
|
|
1215
|
+
const tableDef = schema.tables[key];
|
|
1216
|
+
if (tableDef !== void 0) {
|
|
1217
|
+
tables[key] = makeTypedTable(tableDef.name, raw2);
|
|
515
1218
|
}
|
|
516
|
-
return tables;
|
|
517
1219
|
}
|
|
518
1220
|
const result = {
|
|
519
|
-
tables
|
|
520
|
-
transaction
|
|
1221
|
+
tables,
|
|
1222
|
+
transaction(fn) {
|
|
1223
|
+
const builder = new TxPlanBuilder();
|
|
1224
|
+
const planTables = {};
|
|
1225
|
+
for (const key of Object.keys(schema.tables)) {
|
|
1226
|
+
const tableDef = schema.tables[key];
|
|
1227
|
+
if (tableDef !== void 0) planTables[key] = builder.table(tableDef.name);
|
|
1228
|
+
}
|
|
1229
|
+
return runTxPlan(
|
|
1230
|
+
raw2,
|
|
1231
|
+
planTables,
|
|
1232
|
+
builder,
|
|
1233
|
+
fn
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
521
1236
|
};
|
|
522
1237
|
return result;
|
|
523
1238
|
}
|
|
@@ -631,7 +1346,7 @@ function makeEnvDts(schema) {
|
|
|
631
1346
|
${blocks.join("\n")}
|
|
632
1347
|
` : "";
|
|
633
1348
|
return `// AUTO-GENERATED by @palbase/backend \u2014 DO NOT EDIT.
|
|
634
|
-
// Regenerated from db/schema.ts on every \`palbase
|
|
1349
|
+
// Regenerated from db/schema.ts on every \`palbase db types\` / deploy.
|
|
635
1350
|
// Augments the @palbase/backend/env \`Tables\` interface so \`Database.tables.*\`
|
|
636
1351
|
// is typed with no import and no generic.
|
|
637
1352
|
|
|
@@ -856,12 +1571,12 @@ var E164_RE = /^\+[1-9]\d{1,14}$/;
|
|
|
856
1571
|
var OTP_LENGTH = 6;
|
|
857
1572
|
var OTP_RE = /^\d{6}$/;
|
|
858
1573
|
var MIN_PASSWORD_LENGTH = 8;
|
|
859
|
-
function
|
|
1574
|
+
function isPlainObject2(value) {
|
|
860
1575
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
861
1576
|
}
|
|
862
1577
|
function normalizeSeed(seed, userName) {
|
|
863
1578
|
if (seed === void 0) return {};
|
|
864
|
-
if (!
|
|
1579
|
+
if (!isPlainObject2(seed)) {
|
|
865
1580
|
throw new Error(`testUser ${JSON.stringify(userName)}: seed must be { <table>: [ {...} ] }`);
|
|
866
1581
|
}
|
|
867
1582
|
const out = {};
|
|
@@ -877,7 +1592,7 @@ function normalizeSeed(seed, userName) {
|
|
|
877
1592
|
);
|
|
878
1593
|
}
|
|
879
1594
|
for (const row of rows) {
|
|
880
|
-
if (!
|
|
1595
|
+
if (!isPlainObject2(row)) {
|
|
881
1596
|
throw new Error(
|
|
882
1597
|
`testUser ${JSON.stringify(userName)}: every row in seed.${table} must be an object`
|
|
883
1598
|
);
|
|
@@ -888,7 +1603,7 @@ function normalizeSeed(seed, userName) {
|
|
|
888
1603
|
return out;
|
|
889
1604
|
}
|
|
890
1605
|
function testUser(opts = {}) {
|
|
891
|
-
if (!
|
|
1606
|
+
if (!isPlainObject2(opts)) {
|
|
892
1607
|
throw new Error("testUser() expects { email?, password?, phone?, otp?, seed? }");
|
|
893
1608
|
}
|
|
894
1609
|
const hasEmail = opts.email !== void 0;
|
|
@@ -938,7 +1653,7 @@ function testUser(opts = {}) {
|
|
|
938
1653
|
return { email, password, phone, otp, seed: normalizeSeed(opts.seed, email ?? "<template>") };
|
|
939
1654
|
}
|
|
940
1655
|
function defineTestUsers(input) {
|
|
941
|
-
if (!
|
|
1656
|
+
if (!isPlainObject2(input) || !isPlainObject2(input.users)) {
|
|
942
1657
|
throw new Error("defineTestUsers expects { users: { <name>: testUser({...}) } }");
|
|
943
1658
|
}
|
|
944
1659
|
const users = {};
|
|
@@ -1081,7 +1796,7 @@ var ROUTES = /* @__PURE__ */ Symbol.for("palbase.backend.routes");
|
|
|
1081
1796
|
var PARAM_BUFFER = /* @__PURE__ */ Symbol.for("palbase.backend.paramBuffer");
|
|
1082
1797
|
var RETURN_BUFFER = /* @__PURE__ */ Symbol.for("palbase.backend.returnBuffer");
|
|
1083
1798
|
var THROWS_BUFFER = /* @__PURE__ */ Symbol.for("palbase.backend.throwsBuffer");
|
|
1084
|
-
function
|
|
1799
|
+
function carrierOf2(target) {
|
|
1085
1800
|
const ctor = typeof target === "function" ? target : target.constructor ?? target;
|
|
1086
1801
|
return ctor;
|
|
1087
1802
|
}
|
|
@@ -1098,7 +1813,7 @@ function ownParamBuffer(carrier) {
|
|
|
1098
1813
|
return carrier[PARAM_BUFFER];
|
|
1099
1814
|
}
|
|
1100
1815
|
function recordRoute(target, fnName, method, subpath, options) {
|
|
1101
|
-
const carrier =
|
|
1816
|
+
const carrier = carrierOf2(target);
|
|
1102
1817
|
const routes = ownRoutes(carrier);
|
|
1103
1818
|
const buffer = ownParamBuffer(carrier);
|
|
1104
1819
|
const params = (buffer[fnName] ?? []).slice().sort((a, b) => a.index - b.index);
|
|
@@ -1114,7 +1829,7 @@ function recordRoute(target, fnName, method, subpath, options) {
|
|
|
1114
1829
|
routes.push(route);
|
|
1115
1830
|
}
|
|
1116
1831
|
function recordParam(target, fnName, meta) {
|
|
1117
|
-
const carrier =
|
|
1832
|
+
const carrier = carrierOf2(target);
|
|
1118
1833
|
const buffer = ownParamBuffer(carrier);
|
|
1119
1834
|
(buffer[fnName] ??= []).push(meta);
|
|
1120
1835
|
const routes = carrier[ROUTES];
|
|
@@ -1127,7 +1842,7 @@ function recordParam(target, fnName, meta) {
|
|
|
1127
1842
|
}
|
|
1128
1843
|
}
|
|
1129
1844
|
function recordThrows(target, fnName, throws) {
|
|
1130
|
-
const carrier =
|
|
1845
|
+
const carrier = carrierOf2(target);
|
|
1131
1846
|
const routes = carrier[ROUTES];
|
|
1132
1847
|
const route = routes?.find((r) => r.fnName === fnName);
|
|
1133
1848
|
if (route) {
|
|
@@ -1141,7 +1856,7 @@ function recordThrows(target, fnName, throws) {
|
|
|
1141
1856
|
if (throwsBuffer) throwsBuffer[fnName] = throws;
|
|
1142
1857
|
}
|
|
1143
1858
|
function getRoutes(ctor) {
|
|
1144
|
-
const carrier =
|
|
1859
|
+
const carrier = carrierOf2(ctor);
|
|
1145
1860
|
const routes = carrier[ROUTES] ?? [];
|
|
1146
1861
|
const returnBuffer = carrier[RETURN_BUFFER];
|
|
1147
1862
|
if (returnBuffer) {
|
|
@@ -1277,90 +1992,6 @@ function defineMiddleware(fn) {
|
|
|
1277
1992
|
return fn;
|
|
1278
1993
|
}
|
|
1279
1994
|
|
|
1280
|
-
// src/errors.ts
|
|
1281
|
-
var HttpError = class extends Error {
|
|
1282
|
-
status;
|
|
1283
|
-
error;
|
|
1284
|
-
errorDescription;
|
|
1285
|
-
data;
|
|
1286
|
-
constructor(status, error, errorDescription, data) {
|
|
1287
|
-
super(errorDescription);
|
|
1288
|
-
this.name = "HttpError";
|
|
1289
|
-
this.status = status;
|
|
1290
|
-
this.error = error;
|
|
1291
|
-
this.errorDescription = errorDescription;
|
|
1292
|
-
if (data !== void 0) {
|
|
1293
|
-
this.data = data;
|
|
1294
|
-
}
|
|
1295
|
-
}
|
|
1296
|
-
/**
|
|
1297
|
-
* Serialize to the standard Palbase error response format.
|
|
1298
|
-
* The `requestId` is injected by the runtime layer from the request context.
|
|
1299
|
-
* When called without arguments (e.g. JSON.stringify), request_id is omitted.
|
|
1300
|
-
* When `data` is set, it is appended as a strict-superset field.
|
|
1301
|
-
*/
|
|
1302
|
-
toJSON(requestId) {
|
|
1303
|
-
const result = {
|
|
1304
|
-
error: this.error,
|
|
1305
|
-
error_description: this.errorDescription,
|
|
1306
|
-
status: this.status
|
|
1307
|
-
};
|
|
1308
|
-
if (requestId) {
|
|
1309
|
-
result.request_id = requestId;
|
|
1310
|
-
}
|
|
1311
|
-
if (this.data !== void 0) {
|
|
1312
|
-
result.data = this.data;
|
|
1313
|
-
}
|
|
1314
|
-
return result;
|
|
1315
|
-
}
|
|
1316
|
-
};
|
|
1317
|
-
var PalError = class extends HttpError {
|
|
1318
|
-
constructor(status, code, description, data) {
|
|
1319
|
-
super(status, code, description, data);
|
|
1320
|
-
this.name = "PalError";
|
|
1321
|
-
}
|
|
1322
|
-
};
|
|
1323
|
-
var NamedHttpError = class extends HttpError {
|
|
1324
|
-
constructor(status, defaultCode, name, message, code, data) {
|
|
1325
|
-
super(status, code ?? defaultCode, message ?? defaultMessage(name), data);
|
|
1326
|
-
this.name = name;
|
|
1327
|
-
}
|
|
1328
|
-
};
|
|
1329
|
-
function defaultMessage(name) {
|
|
1330
|
-
const spaced = name.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
|
|
1331
|
-
return spaced.charAt(0).toUpperCase() + spaced.slice(1).toLowerCase();
|
|
1332
|
-
}
|
|
1333
|
-
var BadRequest = class extends NamedHttpError {
|
|
1334
|
-
constructor(data, message) {
|
|
1335
|
-
super(400, "bad_request", "BadRequest", message, void 0, data);
|
|
1336
|
-
}
|
|
1337
|
-
};
|
|
1338
|
-
var Unauthorized = class extends NamedHttpError {
|
|
1339
|
-
constructor(message, code, data) {
|
|
1340
|
-
super(401, "unauthorized", "Unauthorized", message, code, data);
|
|
1341
|
-
}
|
|
1342
|
-
};
|
|
1343
|
-
var Forbidden = class extends NamedHttpError {
|
|
1344
|
-
constructor(message, code, data) {
|
|
1345
|
-
super(403, "forbidden", "Forbidden", message, code, data);
|
|
1346
|
-
}
|
|
1347
|
-
};
|
|
1348
|
-
var NotFound = class extends NamedHttpError {
|
|
1349
|
-
constructor(message, code, data) {
|
|
1350
|
-
super(404, "not_found", "NotFound", message, code, data);
|
|
1351
|
-
}
|
|
1352
|
-
};
|
|
1353
|
-
var Conflict = class extends NamedHttpError {
|
|
1354
|
-
constructor(message, code, data) {
|
|
1355
|
-
super(409, "conflict", "Conflict", message, code, data);
|
|
1356
|
-
}
|
|
1357
|
-
};
|
|
1358
|
-
var TooManyRequests = class extends NamedHttpError {
|
|
1359
|
-
constructor(data, message) {
|
|
1360
|
-
super(429, "too_many_requests", "TooManyRequests", message, void 0, data);
|
|
1361
|
-
}
|
|
1362
|
-
};
|
|
1363
|
-
|
|
1364
1995
|
// src/error-registry.ts
|
|
1365
1996
|
var import_zod_to_openapi = require("@asteasolutions/zod-to-openapi");
|
|
1366
1997
|
var import_zod = require("zod");
|
|
@@ -1370,7 +2001,27 @@ var BUILTIN_DATA_SCHEMAS = {
|
|
|
1370
2001
|
bad_request: import_zod.z.object({
|
|
1371
2002
|
fields: import_zod.z.array(import_zod.z.object({ field: import_zod.z.string(), message: import_zod.z.string() }))
|
|
1372
2003
|
}),
|
|
1373
|
-
too_many_requests: import_zod.z.object({ retryAfter: import_zod.z.number().int() })
|
|
2004
|
+
too_many_requests: import_zod.z.object({ retryAfter: import_zod.z.number().int() }),
|
|
2005
|
+
// Purchases (@RequireEntitlement / @Spend). Their payloads are what make a
|
|
2006
|
+
// paywall renderable on the client: WHICH entitlement was missing, and for a
|
|
2007
|
+
// spend the real ceiling and reset time rather than a guessed retry delay.
|
|
2008
|
+
entitlement_required: import_zod.z.object({ entitlement: import_zod.z.string().nullable() }),
|
|
2009
|
+
quota_exceeded: import_zod.z.object({
|
|
2010
|
+
key: import_zod.z.string(),
|
|
2011
|
+
scope: import_zod.z.string(),
|
|
2012
|
+
window: import_zod.z.string(),
|
|
2013
|
+
used: import_zod.z.number().int(),
|
|
2014
|
+
reserved: import_zod.z.number().int(),
|
|
2015
|
+
max: import_zod.z.number().int(),
|
|
2016
|
+
remaining: import_zod.z.number().int(),
|
|
2017
|
+
resetAt: import_zod.z.string()
|
|
2018
|
+
}),
|
|
2019
|
+
credit_insufficient: import_zod.z.object({
|
|
2020
|
+
key: import_zod.z.string(),
|
|
2021
|
+
balance: import_zod.z.number().int(),
|
|
2022
|
+
reserved: import_zod.z.number().int(),
|
|
2023
|
+
remaining: import_zod.z.number().int()
|
|
2024
|
+
})
|
|
1374
2025
|
};
|
|
1375
2026
|
function getErrorRegistry() {
|
|
1376
2027
|
const g = globalThis;
|
|
@@ -1382,7 +2033,13 @@ function getErrorRegistry() {
|
|
|
1382
2033
|
["forbidden", 403, "Forbidden"],
|
|
1383
2034
|
["not_found", 404, "NotFound"],
|
|
1384
2035
|
["conflict", 409, "Conflict"],
|
|
1385
|
-
["too_many_requests", 429, "TooManyRequests"]
|
|
2036
|
+
["too_many_requests", 429, "TooManyRequests"],
|
|
2037
|
+
// Thrown by the purchases decorators, so they are pre-seeded like the
|
|
2038
|
+
// other built-ins: a project never declares them, but every gated or
|
|
2039
|
+
// metered route must surface them as TYPED errors on the client.
|
|
2040
|
+
["entitlement_required", 403, "EntitlementRequired"],
|
|
2041
|
+
["quota_exceeded", 429, "QuotaExceeded"],
|
|
2042
|
+
["credit_insufficient", 429, "CreditInsufficient"]
|
|
1386
2043
|
]) {
|
|
1387
2044
|
const dataSchema = BUILTIN_DATA_SCHEMAS[code];
|
|
1388
2045
|
m.set(code, {
|
|
@@ -1795,6 +2452,7 @@ var import_zod2 = require("zod");
|
|
|
1795
2452
|
Patch,
|
|
1796
2453
|
PolicyBuilder,
|
|
1797
2454
|
Post,
|
|
2455
|
+
Purchases,
|
|
1798
2456
|
Put,
|
|
1799
2457
|
Query,
|
|
1800
2458
|
QueryParams,
|
|
@@ -1803,12 +2461,16 @@ var import_zod2 = require("zod");
|
|
|
1803
2461
|
Realtime,
|
|
1804
2462
|
Req,
|
|
1805
2463
|
RequestId,
|
|
2464
|
+
RequireEntitlement,
|
|
1806
2465
|
Resource,
|
|
1807
2466
|
STORAGE_CONFIG_KIND,
|
|
2467
|
+
Spend,
|
|
1808
2468
|
Storage,
|
|
1809
2469
|
TEST_USERS_CONFIG_KIND,
|
|
1810
2470
|
TooManyRequests,
|
|
1811
2471
|
TraceId,
|
|
2472
|
+
TxPlanError,
|
|
2473
|
+
TxRefError,
|
|
1812
2474
|
Unauthorized,
|
|
1813
2475
|
Upload,
|
|
1814
2476
|
UploadedObject,
|
|
@@ -1825,6 +2487,7 @@ var import_zod2 = require("zod");
|
|
|
1825
2487
|
boolean,
|
|
1826
2488
|
bucket,
|
|
1827
2489
|
buildProvider,
|
|
2490
|
+
dec,
|
|
1828
2491
|
defineEgress,
|
|
1829
2492
|
defineError,
|
|
1830
2493
|
defineFlags,
|
|
@@ -1837,21 +2500,26 @@ var import_zod2 = require("zod");
|
|
|
1837
2500
|
defineWebhook,
|
|
1838
2501
|
defineWorker,
|
|
1839
2502
|
documents,
|
|
2503
|
+
entitlementFor,
|
|
1840
2504
|
enumType,
|
|
1841
2505
|
flag,
|
|
1842
2506
|
getErrorRegistry,
|
|
1843
2507
|
getRoutes,
|
|
2508
|
+
inc,
|
|
1844
2509
|
integer,
|
|
1845
2510
|
isPalbaseExtension,
|
|
1846
2511
|
jsonb,
|
|
1847
2512
|
makeEnvDts,
|
|
2513
|
+
makePurchasesDts,
|
|
1848
2514
|
makeTypedDB,
|
|
2515
|
+
now,
|
|
1849
2516
|
numeric,
|
|
1850
2517
|
parseFileSizeLimit,
|
|
1851
2518
|
policy,
|
|
1852
2519
|
raw,
|
|
1853
2520
|
recordThrows,
|
|
1854
2521
|
reservedSecretKey,
|
|
2522
|
+
spendFor,
|
|
1855
2523
|
storage,
|
|
1856
2524
|
testUser,
|
|
1857
2525
|
text,
|