@palbase/backend 9.0.1 → 10.1.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/index.js CHANGED
@@ -31,7 +31,7 @@ import {
31
31
  text,
32
32
  timestamp,
33
33
  uuid
34
- } from "./chunk-SO6YV35D.js";
34
+ } from "./chunk-4WOQWFUP.js";
35
35
 
36
36
  // src/db/env-gen.ts
37
37
  function baseTsType(def) {
@@ -64,7 +64,50 @@ function rowType(def) {
64
64
  function optionalOnInsert(def) {
65
65
  return def.nullable === true || def.defaultRandom === true || def.defaultNow === true || def.defaultValue !== void 0;
66
66
  }
67
- function tableBlock(table, indent) {
67
+ var AUTH_USERS_TABLE = "auth.users";
68
+ function ownerColumnOf(table) {
69
+ for (const [col, builder] of Object.entries(table.columns)) {
70
+ if (builder._def.references?.table === AUTH_USERS_TABLE) return col;
71
+ }
72
+ return null;
73
+ }
74
+ function buildChildEdges(schema) {
75
+ const edges = /* @__PURE__ */ new Map();
76
+ for (const name of Object.keys(schema.tables)) edges.set(name, []);
77
+ for (const childName of Object.keys(schema.tables)) {
78
+ const table = schema.tables[childName];
79
+ if (table === void 0) continue;
80
+ for (const [col, builder] of Object.entries(table.columns)) {
81
+ const ref = builder._def.references;
82
+ if (ref === void 0) continue;
83
+ if (ref.table === childName) continue;
84
+ const bucket2 = edges.get(ref.table);
85
+ if (bucket2 === void 0) continue;
86
+ if (bucket2.some((e) => e.child === childName)) continue;
87
+ bucket2.push({ child: childName, column: col });
88
+ }
89
+ }
90
+ return edges;
91
+ }
92
+ function pruneCycles(raw2) {
93
+ const pruned = /* @__PURE__ */ new Map();
94
+ for (const table of raw2.keys()) pruned.set(table, []);
95
+ const state = /* @__PURE__ */ new Map();
96
+ function visit(table) {
97
+ state.set(table, "visiting");
98
+ for (const edge of raw2.get(table) ?? []) {
99
+ if (state.get(edge.child) === "visiting") continue;
100
+ pruned.get(table)?.push(edge);
101
+ if (state.get(edge.child) === void 0) visit(edge.child);
102
+ }
103
+ state.set(table, "done");
104
+ }
105
+ for (const table of raw2.keys()) {
106
+ if (state.get(table) === void 0) visit(table);
107
+ }
108
+ return pruned;
109
+ }
110
+ function tableBlock(table, children, indent) {
68
111
  const cols = Object.entries(table.columns);
69
112
  const rowLines = cols.map(([col, builder]) => {
70
113
  return `${indent} ${col}: ${rowType(builder._def)};`;
@@ -74,6 +117,8 @@ function tableBlock(table, indent) {
74
117
  const opt = optionalOnInsert(def) ? "?" : "";
75
118
  return `${indent} ${col}${opt}: ${rowType(def)};`;
76
119
  });
120
+ const owner = ownerColumnOf(table);
121
+ const childEntries = children.map((e) => `${JSON.stringify(e.child)}: ${JSON.stringify(e.column)}`).join("; ");
77
122
  return [
78
123
  `${indent}${table.name}: {`,
79
124
  `${indent} row: {`,
@@ -82,12 +127,17 @@ function tableBlock(table, indent) {
82
127
  `${indent} insert: {`,
83
128
  ...insertLines,
84
129
  `${indent} };`,
130
+ `${indent} owner: ${owner === null ? "null" : JSON.stringify(owner)};`,
131
+ `${indent} children: {${childEntries === "" ? "" : ` ${childEntries} `}};`,
85
132
  `${indent}};`
86
133
  ].join("\n");
87
134
  }
88
135
  function makeEnvDts(schema) {
89
136
  const tableNames = Object.keys(schema.tables);
90
- const blocks = tableNames.map((name) => tableBlock(schema.tables[name], " "));
137
+ const children = pruneCycles(buildChildEdges(schema));
138
+ const blocks = tableNames.map(
139
+ (name) => tableBlock(schema.tables[name], children.get(name) ?? [], " ")
140
+ );
91
141
  const body = blocks.length > 0 ? `
92
142
  ${blocks.join("\n")}
93
143
  ` : "";
@@ -182,6 +232,22 @@ function defineStorage(input) {
182
232
  return { __config: STORAGE_CONFIG_KIND, buckets };
183
233
  }
184
234
 
235
+ // src/config/egress.ts
236
+ var EGRESS_CONFIG_KIND = "egress";
237
+ function defineEgress(input) {
238
+ if (input === null || typeof input !== "object" || !Array.isArray(input.hosts)) {
239
+ throw new Error("defineEgress expects { hosts: string[] }");
240
+ }
241
+ const hosts = [];
242
+ for (const h of input.hosts) {
243
+ if (typeof h !== "string" || h.trim().length === 0) {
244
+ throw new Error("defineEgress: every host must be a non-empty string");
245
+ }
246
+ hosts.push(h.trim().toLowerCase());
247
+ }
248
+ return { __config: EGRESS_CONFIG_KIND, hosts };
249
+ }
250
+
185
251
  // src/config/notifications.ts
186
252
  var NOTIFICATIONS_CONFIG_KIND = "notifications";
187
253
  var RESERVED_SECRET_PREFIX = "PB_NOTIFICATIONS";
@@ -293,6 +359,124 @@ function camelToUpperSnake(s) {
293
359
  return s.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toUpperCase();
294
360
  }
295
361
 
362
+ // src/config/test-users.ts
363
+ var TEST_USERS_CONFIG_KIND = "test-users";
364
+ var TEST_USER_NAME_RE = /^[a-zA-Z][a-zA-Z0-9_]*$/;
365
+ var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
366
+ var E164_RE = /^\+[1-9]\d{1,14}$/;
367
+ var OTP_LENGTH = 6;
368
+ var OTP_RE = /^\d{6}$/;
369
+ var MIN_PASSWORD_LENGTH = 8;
370
+ function isPlainObject(value) {
371
+ return typeof value === "object" && value !== null && !Array.isArray(value);
372
+ }
373
+ function normalizeSeed(seed, userName) {
374
+ if (seed === void 0) return {};
375
+ if (!isPlainObject(seed)) {
376
+ throw new Error(`testUser ${JSON.stringify(userName)}: seed must be { <table>: [ {...} ] }`);
377
+ }
378
+ const out = {};
379
+ for (const [table, rows] of Object.entries(seed)) {
380
+ if (!Array.isArray(rows)) {
381
+ throw new Error(
382
+ `testUser ${JSON.stringify(userName)}: seed.${table} must be an array of rows`
383
+ );
384
+ }
385
+ if (rows.length === 0) {
386
+ throw new Error(
387
+ `testUser ${JSON.stringify(userName)}: seed.${table} is empty \u2014 remove it or add a row`
388
+ );
389
+ }
390
+ for (const row of rows) {
391
+ if (!isPlainObject(row)) {
392
+ throw new Error(
393
+ `testUser ${JSON.stringify(userName)}: every row in seed.${table} must be an object`
394
+ );
395
+ }
396
+ }
397
+ out[table] = rows;
398
+ }
399
+ return out;
400
+ }
401
+ function testUser(opts = {}) {
402
+ if (!isPlainObject(opts)) {
403
+ throw new Error("testUser() expects { email?, password?, phone?, otp?, seed? }");
404
+ }
405
+ const hasEmail = opts.email !== void 0;
406
+ const hasPassword = opts.password !== void 0;
407
+ if (hasEmail !== hasPassword) {
408
+ throw new Error(
409
+ "testUser() needs email AND password together (a fixture with stable credentials) or neither (a template the server generates credentials for)"
410
+ );
411
+ }
412
+ let email = null;
413
+ let password = null;
414
+ if (hasEmail) {
415
+ if (typeof opts.email !== "string" || !EMAIL_RE.test(opts.email)) {
416
+ throw new Error(`testUser email ${JSON.stringify(opts.email)} is not a valid address`);
417
+ }
418
+ if (typeof opts.password !== "string" || opts.password.length < MIN_PASSWORD_LENGTH) {
419
+ throw new Error(
420
+ `testUser password must be at least ${MIN_PASSWORD_LENGTH} characters (got ${typeof opts.password === "string" ? opts.password.length : 0})`
421
+ );
422
+ }
423
+ email = opts.email;
424
+ password = opts.password;
425
+ }
426
+ const hasPhone = opts.phone !== void 0;
427
+ const hasOTP = opts.otp !== void 0;
428
+ if (hasPhone !== hasOTP) {
429
+ throw new Error(
430
+ "testUser() needs phone AND otp together \u2014 nothing can receive an SMS at a fabricated number, so a phone fixture without its fixed code can never be signed in as"
431
+ );
432
+ }
433
+ let phone = null;
434
+ let otp = null;
435
+ if (hasPhone) {
436
+ if (typeof opts.phone !== "string" || !E164_RE.test(opts.phone)) {
437
+ throw new Error(
438
+ `testUser phone ${JSON.stringify(opts.phone)} must be E.164, e.g. "+905551112233"`
439
+ );
440
+ }
441
+ if (typeof opts.otp !== "string" || !OTP_RE.test(opts.otp)) {
442
+ throw new Error(
443
+ `testUser otp ${JSON.stringify(opts.otp)} must be ${OTP_LENGTH} digits`
444
+ );
445
+ }
446
+ phone = opts.phone;
447
+ otp = opts.otp;
448
+ }
449
+ return { email, password, phone, otp, seed: normalizeSeed(opts.seed, email ?? "<template>") };
450
+ }
451
+ function defineTestUsers(input) {
452
+ if (!isPlainObject(input) || !isPlainObject(input.users)) {
453
+ throw new Error("defineTestUsers expects { users: { <name>: testUser({...}) } }");
454
+ }
455
+ const users = {};
456
+ const emails = /* @__PURE__ */ new Map();
457
+ for (const name of Object.keys(input.users)) {
458
+ if (!TEST_USER_NAME_RE.test(name)) {
459
+ throw new Error(
460
+ `test user name ${JSON.stringify(name)} is invalid \u2014 must start with a letter and contain only letters, digits, and underscores`
461
+ );
462
+ }
463
+ const def = input.users[name];
464
+ if (def === void 0) continue;
465
+ if (def.email !== null) {
466
+ const lower = def.email.toLowerCase();
467
+ const already = emails.get(lower);
468
+ if (already !== void 0) {
469
+ throw new Error(
470
+ `test users ${JSON.stringify(already)} and ${JSON.stringify(name)} declare the same e-mail ${JSON.stringify(def.email)}`
471
+ );
472
+ }
473
+ emails.set(lower, name);
474
+ }
475
+ users[name] = def;
476
+ }
477
+ return { __config: TEST_USERS_CONFIG_KIND, users };
478
+ }
479
+
296
480
  // src/config/flags.ts
297
481
  var FLAGS_CONFIG_KIND = "flags";
298
482
  var FLAG_KEY_RE = /^[a-zA-Z][a-zA-Z0-9_]*$/;
@@ -465,6 +649,33 @@ function recordThrows(target, fnName, throws) {
465
649
  const throwsBuffer = carrier[THROWS_BUFFER];
466
650
  if (throwsBuffer) throwsBuffer[fnName] = throws;
467
651
  }
652
+ function getRoutes(ctor) {
653
+ const carrier = carrierOf(ctor);
654
+ const routes = carrier[ROUTES] ?? [];
655
+ const returnBuffer = carrier[RETURN_BUFFER];
656
+ if (returnBuffer) {
657
+ for (const route of routes) {
658
+ const buffered = returnBuffer[route.fnName];
659
+ if (buffered && route.returnSchema === void 0) {
660
+ route.returnSchema = buffered;
661
+ }
662
+ }
663
+ }
664
+ const throwsBuffer = carrier[THROWS_BUFFER];
665
+ if (throwsBuffer) {
666
+ for (const route of routes) {
667
+ const buffered = throwsBuffer[route.fnName];
668
+ if (buffered && route.throws === void 0) {
669
+ route.throws = buffered;
670
+ }
671
+ }
672
+ }
673
+ return routes.map((r) => ({
674
+ ...r,
675
+ params: r.params.slice(),
676
+ ...r.throws !== void 0 ? { throws: r.throws.slice() } : {}
677
+ }));
678
+ }
468
679
 
469
680
  // src/decorators/methods.ts
470
681
  function makeMethodDecorator(method) {
@@ -1076,6 +1287,7 @@ export {
1076
1287
  Database,
1077
1288
  Delete,
1078
1289
  Documents,
1290
+ EGRESS_CONFIG_KIND,
1079
1291
  EXTENSION_DEPENDENCIES,
1080
1292
  FLAGS_CONFIG_KIND,
1081
1293
  Flags,
@@ -1106,6 +1318,7 @@ export {
1106
1318
  Resource,
1107
1319
  STORAGE_CONFIG_KIND,
1108
1320
  Storage,
1321
+ TEST_USERS_CONFIG_KIND,
1109
1322
  TooManyRequests,
1110
1323
  TraceId,
1111
1324
  Unauthorized,
@@ -1124,6 +1337,7 @@ export {
1124
1337
  boolean,
1125
1338
  bucket,
1126
1339
  buildProvider,
1340
+ defineEgress,
1127
1341
  defineError,
1128
1342
  defineFlags,
1129
1343
  defineJob,
@@ -1131,12 +1345,14 @@ export {
1131
1345
  defineNotifications,
1132
1346
  defineSchema,
1133
1347
  defineStorage,
1348
+ defineTestUsers,
1134
1349
  defineWebhook,
1135
1350
  defineWorker,
1136
1351
  documents,
1137
1352
  enumType,
1138
1353
  flag,
1139
1354
  getErrorRegistry,
1355
+ getRoutes,
1140
1356
  integer,
1141
1357
  isPalbaseExtension,
1142
1358
  jsonb,
@@ -1149,6 +1365,7 @@ export {
1149
1365
  recordThrows,
1150
1366
  reservedSecretKey,
1151
1367
  storage,
1368
+ testUser,
1152
1369
  text,
1153
1370
  timestamp,
1154
1371
  uuid,