@palbase/backend 15.0.0 → 17.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.
@@ -1,481 +1,101 @@
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
- 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
52
  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;
266
- }
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")
343
- },
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
- }
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 signIn(credentials) {
61
+ const result = await call(
62
+ "POST",
63
+ "/auth/login",
64
+ credentials
65
+ );
66
+ bearer = result.access_token;
67
+ return result.user ?? { id: "" };
371
68
  },
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")
69
+ async signOut() {
70
+ await call("POST", "/auth/logout", void 0);
71
+ bearer = null;
387
72
  },
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")
73
+ asAnonymous() {
74
+ bearer = null;
393
75
  }
394
76
  };
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
- }
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
- };
423
77
  }
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
- }
78
+ function safeParse(text) {
79
+ try {
80
+ return JSON.parse(text);
81
+ } catch {
82
+ return text;
437
83
  }
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
84
  }
85
+ var configured = null;
86
+ var api = new Proxy({}, {
87
+ get(_target, prop) {
88
+ configured ??= createTestApi({
89
+ baseUrl: process.env.PALBASE_TEST_BASE_URL ?? "",
90
+ apiKey: process.env.PALBASE_TEST_API_KEY ?? "",
91
+ candidateToken: process.env.PALBASE_TEST_CANDIDATE ?? ""
92
+ });
93
+ return Reflect.get(configured, prop, configured);
94
+ }
95
+ });
477
96
  export {
478
- createMockDB,
479
- createTestContext
97
+ TestApiError,
98
+ api,
99
+ createTestApi
480
100
  };
481
101
  //# sourceMappingURL=index.js.map