@palbase/backend 16.0.0 → 17.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.
@@ -1,481 +1,120 @@
1
- import {
2
- __setRuntime
3
- } from "../chunk-I72YYSEI.js";
4
- import "../chunk-7LAXRLPG.js";
5
-
6
- // src/test/mock-db.ts
7
- function createMockDB() {
8
- const store = /* @__PURE__ */ new Map();
9
- const tracked = {
10
- inserted: /* @__PURE__ */ new Map(),
11
- updated: /* @__PURE__ */ new Map(),
12
- deleted: /* @__PURE__ */ new Map()
13
- };
14
- function rowsOf(table) {
15
- let rows = store.get(table);
16
- if (!rows) {
17
- rows = [];
18
- store.set(table, rows);
19
- }
20
- return rows;
21
- }
22
- function track(map, table, row) {
23
- const list = map.get(table);
24
- if (list) list.push(row);
25
- else map.set(table, [row]);
1
+ // src/test/api.ts
2
+ var TestApiError = class extends Error {
3
+ status;
4
+ error;
5
+ data;
6
+ constructor(method, path, status, body) {
7
+ const envelope = body ?? {};
8
+ const code = envelope.error ?? String(status);
9
+ super(`${method} ${path} \u2192 ${status} ${code}${envelope.error_description ? `: ${envelope.error_description}` : ""}`);
10
+ this.name = "TestApiError";
11
+ this.status = status;
12
+ this.error = code;
13
+ this.data = envelope.data;
26
14
  }
27
- const ops = {
28
- async query(_sql, _params) {
29
- return [];
30
- },
31
- async insert(table, data) {
32
- const record = { id: crypto.randomUUID(), ...data };
33
- rowsOf(table).push(record);
34
- track(tracked.inserted, table, record);
35
- return record;
36
- },
37
- async update(table, id, data) {
38
- const rows = store.get(table) ?? [];
39
- const idx = rows.findIndex((r) => r["id"] === id);
40
- const updated = idx >= 0 ? { ...rows[idx], ...data } : { id, ...data };
41
- if (idx >= 0) {
42
- rows[idx] = updated;
43
- }
44
- track(tracked.updated, table, updated);
45
- return updated;
46
- },
47
- async delete(table, id) {
48
- const rows = store.get(table) ?? [];
49
- const idx = rows.findIndex((r) => r["id"] === id);
50
- if (idx >= 0) rows.splice(idx, 1);
51
- const list = tracked.deleted.get(table);
52
- if (list) list.push(id);
53
- else tracked.deleted.set(table, [id]);
54
- },
55
- async findById(table, id) {
56
- const rows = store.get(table) ?? [];
57
- return rows.find((r) => r["id"] === id) ?? null;
58
- },
59
- async findMany(table, query) {
60
- const rows = store.get(table) ?? [];
61
- if (!query) return rows;
62
- return rows.filter(
63
- (row) => Object.entries(query).every(([key, val]) => row[key] === val)
64
- );
65
- }
66
- };
67
- async function txPlan(plan) {
68
- const snapshot = /* @__PURE__ */ new Map();
69
- for (const [table, rows] of store) snapshot.set(table, [...rows]);
70
- const trackedSnapshot = {
71
- inserted: cloneTracked(tracked.inserted),
72
- updated: cloneTracked(tracked.updated),
73
- deleted: new Map([...tracked.deleted].map(([k, v]) => [k, [...v]]))
74
- };
75
- const results = [];
76
- try {
77
- for (const op of plan.ops) {
78
- const result = applyOp(op, results);
79
- results.push(result);
80
- const failure = guardFailure(op.guard, result.rows.length);
81
- if (failure) throw failure;
82
- }
83
- } catch (err) {
84
- store.clear();
85
- for (const [table, rows] of snapshot) store.set(table, rows);
86
- tracked.inserted = trackedSnapshot.inserted;
87
- tracked.updated = trackedSnapshot.updated;
88
- tracked.deleted = trackedSnapshot.deleted;
89
- throw err;
90
- }
91
- return { results };
92
- }
93
- function applyOp(op, results) {
94
- switch (op.op) {
95
- case "insert": {
96
- const record = { id: crypto.randomUUID(), ...resolveMap(op.values ?? {}, results, null) };
97
- rowsOf(op.table).push(record);
98
- track(tracked.inserted, op.table, record);
99
- return { rows: [record], rows_affected: 1 };
100
- }
101
- case "insertMany": {
102
- const written = (op.rows ?? []).map((row) => {
103
- const record = { id: crypto.randomUUID(), ...resolveMap(row, results, null) };
104
- rowsOf(op.table).push(record);
105
- track(tracked.inserted, op.table, record);
106
- return record;
107
- });
108
- return { rows: written, rows_affected: written.length };
109
- }
110
- case "update": {
111
- const rows = rowsOf(op.table);
112
- const where = resolveMap(op.where ?? {}, results, null);
113
- const written = [];
114
- for (let i = 0; i < rows.length; i++) {
115
- const row = rows[i];
116
- if (!row || !matches(row, where)) continue;
117
- const next = { ...row, ...resolveMap(op.set ?? {}, results, row) };
118
- rows[i] = next;
119
- track(tracked.updated, op.table, next);
120
- written.push(next);
121
- }
122
- return { rows: written, rows_affected: written.length };
123
- }
124
- case "delete": {
125
- const rows = rowsOf(op.table);
126
- const where = resolveMap(op.where ?? {}, results, null);
127
- const removed = rows.filter((row) => matches(row, where));
128
- for (const row of removed) {
129
- rows.splice(rows.indexOf(row), 1);
130
- const id = row["id"];
131
- const list = tracked.deleted.get(op.table);
132
- const key = typeof id === "string" ? id : String(id);
133
- if (list) list.push(key);
134
- else tracked.deleted.set(op.table, [key]);
135
- }
136
- return { rows: removed, rows_affected: removed.length };
137
- }
138
- case "select": {
139
- const where = resolveMap(op.where ?? {}, results, null);
140
- let found = rowsOf(op.table).filter((row) => matches(row, where));
141
- if (op.limit !== void 0) found = found.slice(0, op.limit);
142
- return { rows: found, rows_affected: found.length };
143
- }
144
- }
145
- }
146
- const client = {
147
- ...ops,
148
- txPlan,
149
- // In tests there is no real DB role; `asService()` returns the same
150
- // in-memory client so RLS-bypass code paths still hit the same store and
151
- // tracking maps. The omitted `asService` matches the contract (no
152
- // double-bypass), so callers can't recurse.
153
- asService() {
154
- return client;
155
- },
156
- inserted(table) {
157
- return tracked.inserted.get(table) ?? [];
158
- },
159
- updated(table) {
160
- return tracked.updated.get(table) ?? [];
161
- },
162
- deleted(table) {
163
- return tracked.deleted.get(table) ?? [];
164
- },
165
- seed(table, data) {
166
- store.set(table, [...data]);
167
- }
168
- };
169
- return client;
170
- }
171
- function cloneTracked(map) {
172
- return new Map([...map].map(([k, v]) => [k, [...v]]));
173
- }
174
- function resolveValue(value, results, current, column) {
175
- if (typeof value !== "object" || value === null) return value;
176
- const tagged = value;
177
- if (tagged.$ref) {
178
- const row = results[tagged.$ref.op]?.rows[0];
179
- if (!row) {
180
- throw txRejection(409, "tx_ref_unresolved", {
181
- message: `operation ${tagged.$ref.op} produced no row to reference`
182
- });
183
- }
184
- return row[tagged.$ref.field];
185
- }
186
- if (tagged.$expr) {
187
- const fn = tagged.$expr["fn"];
188
- if (fn === "now") return (/* @__PURE__ */ new Date()).toISOString();
189
- const by = Number(tagged.$expr["by"]);
190
- const base = Number(current?.[column] ?? 0);
191
- return fn === "dec" ? base - by : base + by;
15
+ };
16
+ function required(value, envName) {
17
+ if (!value) {
18
+ throw new Error(
19
+ `${envName} is not set \u2014 the test client has nowhere to send requests. This is set by the deploy that runs your tests; if you are running them by hand, set it yourself.`
20
+ );
192
21
  }
193
22
  return value;
194
23
  }
195
- function resolveMap(map, results, current) {
196
- const out = {};
197
- for (const [key, value] of Object.entries(map)) {
198
- out[key] = resolveValue(value, results, current, key);
24
+ function createTestApi(config) {
25
+ const baseUrl = required(config.baseUrl, "PALBASE_TEST_BASE_URL").replace(/\/$/, "");
26
+ const apiKey = required(config.apiKey, "PALBASE_TEST_API_KEY");
27
+ const candidateToken = required(config.candidateToken, "PALBASE_TEST_CANDIDATE");
28
+ const requests = [];
29
+ let bearer = null;
30
+ async function call(method, path, body, opts = {}) {
31
+ const headers = {
32
+ apikey: apiKey,
33
+ // Selects the release under test. Omit it and the gateway serves the LIVE
34
+ // one, which would make the whole suite grade the wrong code.
35
+ "x-palbase-candidate": candidateToken,
36
+ ...opts.headers
37
+ };
38
+ if (bearer) headers.authorization = `Bearer ${bearer}`;
39
+ if (body !== void 0) headers["content-type"] = "application/json";
40
+ const startedAt = Date.now();
41
+ const res = await fetch(`${baseUrl}${path}`, {
42
+ method,
43
+ headers,
44
+ body: body === void 0 ? void 0 : JSON.stringify(body)
45
+ });
46
+ const text = await res.text();
47
+ const parsed = text ? safeParse(text) : void 0;
48
+ requests.push({ method, path, status: res.status, ms: Date.now() - startedAt });
49
+ if (!res.ok) throw new TestApiError(method, path, res.status, parsed);
50
+ return parsed;
199
51
  }
200
- return out;
201
- }
202
- function matches(row, where) {
203
- return Object.entries(where).every(
204
- ([key, value]) => value === null ? row[key] === null || row[key] === void 0 : row[key] === value
205
- );
206
- }
207
- function guardFailure(guard, count) {
208
- if (!guard) return null;
209
- const ok = guard.kind === "one" ? count === 1 : guard.kind === "none" ? count === 0 : guard.kind === "atLeast" ? count >= guard.n : count <= guard.n;
210
- if (ok) return null;
211
- return txRejection(409, "tx_guard_failed", {
212
- slot: guard.slot,
213
- message: `expected ${guard.kind} ${guard.n} row(s), got ${count}`
214
- });
215
- }
216
- function txRejection(status, code, extra) {
217
- const err = new Error(extra.message);
218
- err.status = status;
219
- err.error_code = code;
220
- if (extra.slot !== void 0) err.slot = extra.slot;
221
- return err;
222
- }
223
-
224
- // src/test/context.ts
225
- function createMockLogger(logs) {
226
52
  return {
227
- info(message, ...args) {
228
- logs.push({ level: "info", message, args });
229
- },
230
- warn(message, ...args) {
231
- logs.push({ level: "warn", message, args });
232
- },
233
- error(message, ...args) {
234
- logs.push({ level: "error", message, args });
235
- },
236
- debug(message, ...args) {
237
- logs.push({ level: "debug", message, args });
238
- }
239
- };
240
- }
241
- function createMockCache() {
242
- const store = /* @__PURE__ */ new Map();
243
- const get = async (key) => {
244
- return store.has(key) ? store.get(key) : null;
245
- };
246
- const set = async (key, value, _ttl) => {
247
- store.set(key, value);
248
- };
249
- return {
250
- get,
251
- set,
252
- async del(key) {
253
- store.delete(key);
254
- },
255
- async incr(key) {
256
- const raw = store.get(key);
257
- const current = typeof raw === "number" ? raw : parseInt(String(raw ?? "0"), 10);
258
- const next = current + 1;
259
- store.set(key, next);
260
- return next;
261
- },
262
- async getOrSet(key, ttl, fn) {
263
- const hit = await get(key);
264
- if (hit !== null) {
265
- return hit;
53
+ requests,
54
+ get: (path, opts) => call("GET", path, void 0, opts),
55
+ post: (path, body, opts) => call("POST", path, body, opts),
56
+ patch: (path, body, opts) => call("PATCH", path, body, opts),
57
+ put: (path, body, opts) => call("PUT", path, body, opts),
58
+ delete: (path, opts) => call("DELETE", path, void 0, opts),
59
+ query: (path, body, opts) => call("QUERY", path, body, opts),
60
+ async signInAs(name) {
61
+ const identity = (config.identities ?? {})[name];
62
+ if (!identity) {
63
+ const declared = Object.keys(config.identities ?? {});
64
+ throw new Error(
65
+ `no test identity named ${JSON.stringify(name)} \u2014 the deploy mints one per user declared in config/test-users.ts` + (declared.length ? `; this run has: ${declared.join(", ")}` : ", and this project declares none")
66
+ );
266
67
  }
267
- const value = await fn();
268
- await set(key, value, ttl);
269
- return value;
270
- }
271
- };
272
- }
273
- function createMockModuleClients() {
274
- const notImpl = (label) => {
275
- throw new Error(
276
- `${label} not configured in test mock \u2014 override the matching client on the returned context`
277
- );
278
- };
279
- const docs = {
280
- collection: () => notImpl("docs.collection"),
281
- doc: () => notImpl("docs.doc")
282
- };
283
- const auth = {
284
- verifyUserToken: () => notImpl("auth.verifyUserToken"),
285
- getSession: () => notImpl("auth.getSession"),
286
- mfa: {
287
- enroll: () => notImpl("auth.mfa.enroll"),
288
- verifyEnrollment: () => notImpl("auth.mfa.verifyEnrollment"),
289
- challenge: () => notImpl("auth.mfa.challenge"),
290
- recovery: () => notImpl("auth.mfa.recovery"),
291
- listFactors: () => notImpl("auth.mfa.listFactors"),
292
- removeFactor: () => notImpl("auth.mfa.removeFactor"),
293
- regenerateRecoveryCodes: () => notImpl("auth.mfa.regenerateRecoveryCodes"),
294
- emailEnroll: () => notImpl("auth.mfa.emailEnroll"),
295
- emailChallenge: () => notImpl("auth.mfa.emailChallenge"),
296
- emailVerify: () => notImpl("auth.mfa.emailVerify")
297
- },
298
- device: {
299
- generateChallenge: () => notImpl("auth.device.generateChallenge"),
300
- attestAndroid: () => notImpl("auth.device.attestAndroid"),
301
- attestiOS: () => notImpl("auth.device.attestiOS"),
302
- bind: () => notImpl("auth.device.bind"),
303
- list: () => notImpl("auth.device.list"),
304
- delete: () => notImpl("auth.device.delete"),
305
- verifyRequestSignature: () => notImpl("auth.device.verifyRequestSignature"),
306
- getToken: () => notImpl("auth.device.getToken"),
307
- get isActive() {
308
- return notImpl("auth.device.isActive");
309
- },
310
- setCachedToken: () => notImpl("auth.device.setCachedToken"),
311
- dispose: () => notImpl("auth.device.dispose")
312
- }
313
- };
314
- const storage = {
315
- bucket: () => notImpl("storage.bucket")
316
- };
317
- const realtime = {
318
- broadcast: async () => ({ data: void 0, error: null })
319
- };
320
- const functions = {
321
- invoke: () => notImpl("functions.invoke")
322
- };
323
- const flags = {
324
- isEnabled: () => notImpl("flags.isEnabled"),
325
- getVariant: () => notImpl("flags.getVariant"),
326
- getAll: () => notImpl("flags.getAll"),
327
- setOverride: () => notImpl("flags.setOverride"),
328
- asService: () => ({
329
- setOverrideForUser: () => notImpl("flags.asService.setOverrideForUser"),
330
- setOverridesForUser: () => notImpl("flags.asService.setOverridesForUser"),
331
- clearOverrideForUser: () => notImpl("flags.asService.clearOverrideForUser"),
332
- clearAllOverridesForUser: () => notImpl("flags.asService.clearAllOverridesForUser"),
333
- batchSetOverrides: () => notImpl("flags.asService.batchSetOverrides")
334
- })
335
- };
336
- const notifications = {
337
- push: { send: () => notImpl("notifications.push.send") },
338
- email: { send: () => notImpl("notifications.email.send") },
339
- sms: { send: () => notImpl("notifications.sms.send") },
340
- verifications: {
341
- start: () => notImpl("notifications.verifications.start"),
342
- check: () => notImpl("notifications.verifications.check")
68
+ return this.signIn(identity);
343
69
  },
344
- inbox: {
345
- send: () => notImpl("notifications.inbox.send"),
346
- list: () => notImpl("notifications.inbox.list"),
347
- unreadCount: () => notImpl("notifications.inbox.unreadCount"),
348
- markRead: () => notImpl("notifications.inbox.markRead"),
349
- markAllRead: () => notImpl("notifications.inbox.markAllRead"),
350
- archive: () => notImpl("notifications.inbox.archive")
351
- },
352
- preferences: {
353
- get: () => notImpl("notifications.preferences.get"),
354
- update: () => notImpl("notifications.preferences.update")
355
- },
356
- templates: {
357
- email: {
358
- list: () => notImpl("notifications.templates.email.list"),
359
- get: () => notImpl("notifications.templates.email.get"),
360
- create: () => notImpl("notifications.templates.email.create"),
361
- update: () => notImpl("notifications.templates.email.update"),
362
- delete: () => notImpl("notifications.templates.email.delete")
363
- },
364
- sms: {
365
- list: () => notImpl("notifications.templates.sms.list"),
366
- get: () => notImpl("notifications.templates.sms.get"),
367
- create: () => notImpl("notifications.templates.sms.create"),
368
- update: () => notImpl("notifications.templates.sms.update"),
369
- delete: () => notImpl("notifications.templates.sms.delete")
370
- }
70
+ async signIn(credentials) {
71
+ const result = await call(
72
+ "POST",
73
+ "/auth/login",
74
+ credentials
75
+ );
76
+ bearer = result.access_token;
77
+ return result.user ?? { id: "" };
371
78
  },
372
- registerDevice: () => notImpl("notifications.registerDevice"),
373
- unregisterDevice: () => notImpl("notifications.unregisterDevice")
374
- };
375
- const analytics = {
376
- capture: () => notImpl("analytics.capture"),
377
- identify: () => notImpl("analytics.identify"),
378
- screen: () => notImpl("analytics.screen"),
379
- query: {
380
- count: () => notImpl("analytics.query.count"),
381
- events: () => notImpl("analytics.query.events"),
382
- properties: () => notImpl("analytics.query.properties"),
383
- users: () => notImpl("analytics.query.users"),
384
- funnel: () => notImpl("analytics.query.funnel"),
385
- retention: () => notImpl("analytics.query.retention"),
386
- cohort: () => notImpl("analytics.query.cohort")
79
+ async signOut() {
80
+ await call("POST", "/auth/logout", void 0);
81
+ bearer = null;
387
82
  },
388
- management: {
389
- overview: () => notImpl("analytics.management.overview"),
390
- eventNames: () => notImpl("analytics.management.eventNames"),
391
- userDetail: () => notImpl("analytics.management.userDetail"),
392
- deleteUser: () => notImpl("analytics.management.deleteUser")
83
+ asAnonymous() {
84
+ bearer = null;
393
85
  }
394
86
  };
395
- const links = {
396
- create: () => notImpl("links.create"),
397
- list: () => notImpl("links.list"),
398
- get: () => notImpl("links.get"),
399
- update: () => notImpl("links.update"),
400
- delete: () => notImpl("links.delete"),
401
- analytics: () => notImpl("links.analytics"),
402
- qrCode: () => notImpl("links.qrCode"),
403
- match: () => notImpl("links.match")
404
- };
405
- return {
406
- auth,
407
- storage,
408
- docs,
409
- realtime,
410
- functions,
411
- flags,
412
- notifications,
413
- analytics,
414
- links
415
- };
416
87
  }
417
- function createMockPurchases() {
418
- return {
419
- resolveSubject: async ({ userRef }) => ({ subjectId: `psj_test_${userRef}` }),
420
- require: async () => void 0,
421
- withSpend: async (_subjectId, _key, _opts, handler) => handler()
422
- };
88
+ function parseIdentities(raw) {
89
+ if (!raw) return {};
90
+ try {
91
+ return JSON.parse(raw);
92
+ } catch {
93
+ return {};
94
+ }
423
95
  }
424
- var NULL_CLIENT_INFO = {
425
- sdkVersion: null,
426
- appVersion: null,
427
- platform: null,
428
- osVersion: null
429
- };
430
- function createTestContext(options = {}) {
431
- const logs = [];
432
- const db = createMockDB();
433
- if (options.db?.seed) {
434
- for (const [table, data] of Object.entries(options.db.seed)) {
435
- db.seed(table, data);
436
- }
96
+ function safeParse(text) {
97
+ try {
98
+ return JSON.parse(text);
99
+ } catch {
100
+ return text;
437
101
  }
438
- const log = createMockLogger(logs);
439
- const cache = createMockCache();
440
- const moduleClients = createMockModuleClients();
441
- __setRuntime({
442
- Database: db,
443
- Documents: moduleClients.docs,
444
- Storage: moduleClients.storage,
445
- Cache: cache,
446
- Log: log,
447
- Notifications: moduleClients.notifications,
448
- Flags: moduleClients.flags,
449
- Realtime: moduleClients.realtime,
450
- Purchases: createMockPurchases()
451
- });
452
- const ctx = {
453
- input: options.input ?? {},
454
- params: options.params ?? {},
455
- query: options.query ?? {},
456
- headers: options.headers ?? {},
457
- user: options.user ?? null,
458
- client: NULL_CLIENT_INFO,
459
- method: "POST",
460
- file: null,
461
- db,
462
- env: options.env ?? {},
463
- log,
464
- cache,
465
- ...moduleClients,
466
- // Empty errors map in tests by default. Tests that exercise an endpoint's
467
- // declared errors construct their own throwers; this stub satisfies the
468
- // PBRequest shape without forcing every test to declare `errors:`.
469
- errors: {},
470
- requestId: "req_test_000000000000",
471
- traceId: "0".repeat(32),
472
- spanId: "0".repeat(16),
473
- logs
474
- };
475
- return ctx;
476
102
  }
103
+ var configured = null;
104
+ var api = new Proxy({}, {
105
+ get(_target, prop) {
106
+ configured ??= createTestApi({
107
+ baseUrl: process.env.PALBASE_TEST_BASE_URL ?? "",
108
+ apiKey: process.env.PALBASE_TEST_API_KEY ?? "",
109
+ candidateToken: process.env.PALBASE_TEST_CANDIDATE ?? "",
110
+ identities: parseIdentities(process.env.PALBASE_TEST_IDENTITIES)
111
+ });
112
+ return Reflect.get(configured, prop, configured);
113
+ }
114
+ });
477
115
  export {
478
- createMockDB,
479
- createTestContext
116
+ TestApiError,
117
+ api,
118
+ createTestApi
480
119
  };
481
120
  //# sourceMappingURL=index.js.map