sporades 0.8.3 → 0.8.5

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/CHANGES.md CHANGED
@@ -6,12 +6,22 @@ Changes since v0.8.1.
6
6
 
7
7
  ### 🚀 Features
8
8
 
9
+ - Add reactive query arguments (5b882f5).
9
10
  - Add exact pagination and join admission (63f68a0).
10
11
 
11
12
  ### 🐛 Bug Fixes
12
13
 
13
14
  - Resolve npm audit vulnerabilities (a6a4b51).
14
15
 
16
+ ### 📝 Documentation
17
+
18
+ - Describe parameterized queries (287ef63).
19
+ - Plan reactive query arguments (4876db8).
20
+
21
+ ### 📦 Packaging
22
+
23
+ - Release v0.8.3 (c17bb1b).
24
+
15
25
  ## 0.8.1 - 2026-08-14
16
26
 
17
27
  Corrects the incomplete `0.8.0` package release with the merged `main` runtime,
@@ -418,7 +418,7 @@ function writeEnvelope(result, failed = false) {
418
418
  }
419
419
 
420
420
  // src/cli/cli-version.ts
421
- var CLI_VERSION = "0.8.3";
421
+ var CLI_VERSION = "0.8.5";
422
422
 
423
423
  // src/cli/schedule-inspection-envelope.ts
424
424
  function sanitizeScheduleInspectionEnvelope(envelope, invalid) {
package/bin/sporades.js CHANGED
@@ -38,11 +38,69 @@ export function onMessage(listener) {
38
38
  }
39
39
 
40
40
  export const queries = {
41
- subscribe(name, listener) {
42
- return connect().subscribeQuery(name, listener);
41
+ subscribe(name, listener, ...args) {
42
+ const normalizedArgs = args.length === 1 && args[0]?.[normalizedQueryArgumentsMarker]
43
+ ? args[0]
44
+ : normalizeQueryArguments(args);
45
+ return connect().subscribeNormalizedQuery(name, listener, normalizedArgs);
43
46
  },
44
47
  };
45
48
 
49
+ const queryArgumentLimitBytes = 65536;
50
+ const normalizedQueryArgumentsMarker = Symbol("sporades.normalizedQueryArguments");
51
+
52
+ function normalizeQueryArguments(args) {
53
+ if (!Array.isArray(args)) throw new TypeError("Query arguments must be an array.");
54
+ const ancestors = new Set();
55
+ const snapshot = normalizeQueryArgumentValue(args, ancestors);
56
+ const identity = JSON.stringify(snapshot);
57
+ const byteLength = new TextEncoder().encode(identity).byteLength;
58
+ if (byteLength > queryArgumentLimitBytes) throw new RangeError("Query arguments exceed the 65536-byte limit.");
59
+ return Object.freeze({ snapshot, identity, byteLength, [normalizedQueryArgumentsMarker]: true });
60
+ }
61
+
62
+ function normalizeQueryArgumentValue(value, ancestors) {
63
+ if (value === null || typeof value === "string" || typeof value === "boolean") return value;
64
+ if (typeof value === "number") {
65
+ if (!Number.isFinite(value)) throw new TypeError("Query arguments must contain JSON values.");
66
+ return value;
67
+ }
68
+ if (Array.isArray(value)) {
69
+ if (Object.getOwnPropertySymbols(value).length > 0) throw new TypeError("Query arguments must not contain symbol keys.");
70
+ if (ancestors.has(value)) throw new TypeError("Query arguments must not contain cycles.");
71
+ if (Object.getOwnPropertyNames(value).some((key) => key !== "length" && (!/^(0|[1-9]\\d*)$/.test(key) || Number(key) >= value.length))) {
72
+ throw new TypeError("Query arguments must not contain non-index array properties.");
73
+ }
74
+ ancestors.add(value);
75
+ const snapshot = [];
76
+ for (let index = 0; index < value.length; index += 1) {
77
+ if (!Object.hasOwn(value, index)) throw new TypeError("Query arguments must not contain sparse arrays.");
78
+ snapshot.push(normalizeQueryArgumentValue(value[index], ancestors));
79
+ }
80
+ ancestors.delete(value);
81
+ return Object.freeze(snapshot);
82
+ }
83
+ if (!value || typeof value !== "object") throw new TypeError("Query arguments must contain JSON values.");
84
+ if (Object.getOwnPropertySymbols(value).length > 0) throw new TypeError("Query arguments must not contain symbol keys.");
85
+ const prototype = Object.getPrototypeOf(value);
86
+ if (prototype !== Object.prototype && prototype !== null) throw new TypeError("Query arguments must contain plain JSON objects.");
87
+ if (ancestors.has(value)) throw new TypeError("Query arguments must not contain cycles.");
88
+ ancestors.add(value);
89
+ const snapshot = Object.create(null);
90
+ for (const key of Object.getOwnPropertyNames(value).sort()) {
91
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
92
+ if (!descriptor || !descriptor.enumerable || !Object.hasOwn(descriptor, "value")) throw new TypeError("Query arguments must contain plain JSON objects.");
93
+ Object.defineProperty(snapshot, key, {
94
+ value: normalizeQueryArgumentValue(descriptor.value, ancestors),
95
+ enumerable: true,
96
+ configurable: false,
97
+ writable: false,
98
+ });
99
+ }
100
+ ancestors.delete(value);
101
+ return Object.freeze(snapshot);
102
+ }
103
+
46
104
  export const mutations = {
47
105
  run(name, ...args) {
48
106
  return connect().mutate(name, args);
@@ -171,13 +229,14 @@ export const files = {
171
229
  export function createHooks(primitives) {
172
230
  const { useEffect, useState } = primitives;
173
231
 
174
- function useQuery(name) {
232
+ function useQuery(name, ...args) {
233
+ const normalizedArgs = normalizeQueryArguments(args);
175
234
  const [state, setState] = useState({ data: null, error: null, loading: true });
176
235
 
177
236
  useEffect(() => {
178
- const subscription = queries.subscribe(name, setState);
237
+ const subscription = queries.subscribe(name, setState, normalizedArgs);
179
238
  return () => subscription.unsubscribe();
180
- }, [name]);
239
+ }, [name, normalizedArgs.identity]);
181
240
 
182
241
  return state;
183
242
  }
@@ -237,9 +296,10 @@ export function createHooks(primitives) {
237
296
  export function createVueComposables(primitives) {
238
297
  const { reactive, onScopeDispose } = primitives;
239
298
 
240
- function useQuery(name) {
299
+ function useQuery(name, ...args) {
300
+ const normalizedArgs = normalizeQueryArguments(args);
241
301
  const state = reactive({ data: null, error: null, loading: true });
242
- const subscription = queries.subscribe(name, (nextState) => Object.assign(state, nextState));
302
+ const subscription = queries.subscribe(name, (nextState) => Object.assign(state, nextState), normalizedArgs);
243
303
  onScopeDispose(() => subscription.unsubscribe());
244
304
  return state;
245
305
  }
@@ -293,9 +353,10 @@ export function createVueComposables(primitives) {
293
353
  export function createSolidPrimitives(primitives) {
294
354
  const { createSignal, onCleanup } = primitives;
295
355
 
296
- function createQuery(name) {
356
+ function createQuery(name, ...args) {
357
+ const normalizedArgs = normalizeQueryArguments(args);
297
358
  const [state, setState] = createSignal({ data: null, error: null, loading: true });
298
- const subscription = queries.subscribe(name, setState);
359
+ const subscription = queries.subscribe(name, setState, normalizedArgs);
299
360
  onCleanup(() => subscription.unsubscribe());
300
361
  return state;
301
362
  }
@@ -389,8 +450,9 @@ export function createLitControllers() {
389
450
  return controller;
390
451
  }
391
452
 
392
- function queryController(host, name) {
393
- return observedController(host, { data: null, error: null, loading: true }, (publish) => queries.subscribe(name, publish));
453
+ function queryController(host, name, ...args) {
454
+ const normalizedArgs = normalizeQueryArguments(args);
455
+ return observedController(host, { data: null, error: null, loading: true }, (publish) => queries.subscribe(name, publish, normalizedArgs));
394
456
  }
395
457
 
396
458
  function mutationController(host, name) {
@@ -477,7 +539,10 @@ export function createInfernoAdapters() {
477
539
  };
478
540
  return adapter;
479
541
  }
480
- const queryAdapter = (host, name) => observedAdapter(host, { data: null, error: null, loading: true }, (publish) => queries.subscribe(name, publish));
542
+ const queryAdapter = (host, name, ...args) => {
543
+ const normalizedArgs = normalizeQueryArguments(args);
544
+ return observedAdapter(host, { data: null, error: null, loading: true }, (publish) => queries.subscribe(name, publish, normalizedArgs));
545
+ };
481
546
  function mutationAdapter(host, name) {
482
547
  let pending = 0, latestInvocation = 0;
483
548
  const adapter = { state: { data: null, error: null, loading: false }, async run(...args) {
@@ -513,10 +578,11 @@ export function createInfernoAdapters() {
513
578
  }
514
579
 
515
580
  export function createSvelteStores() {
516
- function queryStore(name) {
581
+ function queryStore(name, ...args) {
582
+ const normalizedArgs = normalizeQueryArguments(args);
517
583
  return createLazyStore(
518
584
  { data: null, error: null, loading: true },
519
- (publish) => queries.subscribe(name, publish).unsubscribe,
585
+ (publish) => queries.subscribe(name, publish, normalizedArgs).unsubscribe,
520
586
  true,
521
587
  );
522
588
  }
@@ -688,6 +754,7 @@ function createConnection() {
688
754
  id: subscription.id,
689
755
  type: "query.subscribe",
690
756
  query: subscription.name,
757
+ args: subscription.args.snapshot,
691
758
  });
692
759
  }
693
760
  });
@@ -1073,17 +1140,19 @@ function createConnection() {
1073
1140
  confirmPasswordReset(code, newPassword) {
1074
1141
  return request("auth.confirmPasswordReset", { code, newPassword });
1075
1142
  },
1076
- subscribeQuery(name, listener) {
1143
+ subscribeNormalizedQuery(name, listener, args) {
1077
1144
  if (typeof name !== "string" || !name) throw new TypeError("queries.subscribe requires a query name.");
1078
1145
  if (typeof listener !== "function") throw new TypeError("queries.subscribe requires a listener function.");
1079
- let subscription = queryChannels.get(name);
1146
+ const channelsForName = queryChannels.get(name) ?? new Map();
1147
+ if (!queryChannels.has(name)) queryChannels.set(name, channelsForName);
1148
+ let subscription = channelsForName.get(args.identity);
1080
1149
  if (!subscription) {
1081
1150
  const id = nextId++;
1082
- subscription = { id, name, listeners: new Set(), latest: null };
1083
- queryChannels.set(name, subscription);
1151
+ subscription = { id, name, args, listeners: new Set(), latest: null };
1152
+ channelsForName.set(args.identity, subscription);
1084
1153
  subscriptions.set(id, subscription);
1085
1154
  const activeSocket = open();
1086
- if (activeSocket.readyState === WebSocket.OPEN) send({ id, type: "query.subscribe", query: name });
1155
+ if (activeSocket.readyState === WebSocket.OPEN) send({ id, type: "query.subscribe", query: name, args: args.snapshot });
1087
1156
  }
1088
1157
  subscription.listeners.add(listener);
1089
1158
  listener(subscription.latest ?? { data: null, error: null, loading: true });
@@ -1093,7 +1162,8 @@ function createConnection() {
1093
1162
  active = false;
1094
1163
  subscription.listeners.delete(listener);
1095
1164
  if (subscription.listeners.size === 0) {
1096
- queryChannels.delete(name);
1165
+ channelsForName.delete(args.identity);
1166
+ if (channelsForName.size === 0) queryChannels.delete(name);
1097
1167
  subscriptions.delete(subscription.id);
1098
1168
  sendIfOpen({ id: nextId++, type: "query.unsubscribe", subscriptionId: subscription.id });
1099
1169
  }
@@ -16621,7 +16691,30 @@ function createWebSocketHub(getDatabase, trustedRefresh = null) {
16621
16691
  });
16622
16692
  return;
16623
16693
  }
16624
- const subscription = { id: message.id, name: queryName, style: message.query ? "direct" : "rows", generation: 0 };
16694
+ let args;
16695
+ try {
16696
+ args = normalizeQueryArguments(message.args ?? []);
16697
+ } catch {
16698
+ sendJson(client, {
16699
+ id: message.id,
16700
+ type: "query.result",
16701
+ query: queryName,
16702
+ data: null,
16703
+ error: invalidQueryArgumentsError()
16704
+ });
16705
+ return;
16706
+ }
16707
+ if (!message.query && args.length > 0) {
16708
+ sendJson(client, {
16709
+ id: message.id,
16710
+ type: "query.result",
16711
+ query: queryName,
16712
+ data: null,
16713
+ error: invalidQueryArgumentsError()
16714
+ });
16715
+ return;
16716
+ }
16717
+ const subscription = { id: message.id, name: queryName, args, style: message.query ? "direct" : "rows", generation: 0 };
16625
16718
  client.subscriptions.set(message.id, subscription);
16626
16719
  void sendQueryResult(client, subscription, (error) => sendUnhandledMessageError(client, rawMessage, error));
16627
16720
  return;
@@ -17006,7 +17099,7 @@ function createWebSocketHub(getDatabase, trustedRefresh = null) {
17006
17099
  subscription.generation = generation;
17007
17100
  try {
17008
17101
  const database = getDatabase();
17009
- const result = await runQuery(database, client.session.auth, subscription.name);
17102
+ const result = await runQuery(database, client.session.auth, subscription.name, subscription.args);
17010
17103
  const data = subscription.style === "direct" ? result.data ?? result.rows : { rows: result.data ?? result.rows };
17011
17104
  if (client.subscriptions.get(subscription.id) !== subscription || subscription.generation !== generation) return;
17012
17105
  sendJson(client, {
@@ -17228,7 +17321,13 @@ function sendJsonWithCompletion(client, message, timeoutMs = 250) {
17228
17321
  }
17229
17322
  });
17230
17323
  }
17231
- async function runQuery(database, auth, queryName) {
17324
+ async function runQuery(database, auth, queryName, rawArgs = []) {
17325
+ let args;
17326
+ try {
17327
+ args = normalizeQueryArguments(rawArgs);
17328
+ } catch {
17329
+ return { rows: null, data: null, error: invalidQueryArgumentsError() };
17330
+ }
17232
17331
  let context;
17233
17332
  try {
17234
17333
  context = await applyContextMiddleware(database, createMutationContext(database, auth), "query");
@@ -17242,9 +17341,10 @@ async function runQuery(database, auth, queryName) {
17242
17341
  };
17243
17342
  }
17244
17343
  if (queryName === "ctx.env") {
17344
+ if (args.length > 0) return { rows: null, data: null, error: invalidQueryArgumentsError() };
17245
17345
  return { data: context.env, error: null };
17246
17346
  }
17247
- const customResult = await runCustomQuery(database, context, queryName);
17347
+ const customResult = await runCustomQuery(database, context, queryName, args);
17248
17348
  if (customResult) {
17249
17349
  return customResult;
17250
17350
  }
@@ -17258,6 +17358,7 @@ async function runQuery(database, auth, queryName) {
17258
17358
  }
17259
17359
  };
17260
17360
  }
17361
+ if (args.length > 0) return { rows: null, data: null, error: invalidQueryArgumentsError() };
17261
17362
  const cacheKey = `${table.name}:${context.auth.userId}`;
17262
17363
  if (!database.rowCache.has(cacheKey)) {
17263
17364
  const columns = ["id", "createdAt", "updatedAt", ...table.fields.map((field) => field.name)];
@@ -17272,14 +17373,14 @@ async function runQuery(database, auth, queryName) {
17272
17373
  const rows = await filterRowsByReadAcl(database, table, database.rowCache.get(cacheKey), context);
17273
17374
  return { rows, error: null };
17274
17375
  }
17275
- async function runCustomQuery(database, context, queryName) {
17376
+ async function runCustomQuery(database, context, queryName, args) {
17276
17377
  const handler = database.queries.find((candidate) => candidate.name === queryName);
17277
17378
  if (!handler) {
17278
17379
  return null;
17279
17380
  }
17280
17381
  try {
17281
17382
  const queryHandler = typeof handler.handler === "function" ? handler.handler : new Function(`return (${handler.handlerSource});`)();
17282
- const data = await queryHandler(context);
17383
+ const data = await queryHandler(context, ...args);
17283
17384
  assertJsonCompatible(data);
17284
17385
  return { data, error: null };
17285
17386
  } catch (error) {
@@ -17296,6 +17397,64 @@ async function runCustomQuery(database, context, queryName) {
17296
17397
  };
17297
17398
  }
17298
17399
  }
17400
+ var QUERY_ARGUMENT_LIMIT_BYTES = 65536;
17401
+ function invalidQueryArgumentsError() {
17402
+ return {
17403
+ message: "Invalid query arguments.",
17404
+ hint: "Use a JSON-compatible argument array no larger than 65536 UTF-8 bytes."
17405
+ };
17406
+ }
17407
+ function normalizeQueryArguments(value) {
17408
+ if (!Array.isArray(value)) throw new Error("Query arguments must be an array.");
17409
+ const snapshot = normalizeQueryArgumentValue(value, /* @__PURE__ */ new Set());
17410
+ const canonicalJson = JSON.stringify(snapshot);
17411
+ if (Buffer.byteLength(canonicalJson, "utf8") > QUERY_ARGUMENT_LIMIT_BYTES) {
17412
+ throw new Error("Query arguments are too large.");
17413
+ }
17414
+ return snapshot;
17415
+ }
17416
+ function normalizeQueryArgumentValue(value, ancestors) {
17417
+ if (value === null || typeof value === "string" || typeof value === "boolean") return value;
17418
+ if (typeof value === "number") {
17419
+ if (!Number.isFinite(value)) throw new Error("Query arguments must contain finite numbers.");
17420
+ return value;
17421
+ }
17422
+ if (typeof value !== "object") throw new Error("Query arguments must be JSON-compatible.");
17423
+ if (Object.getOwnPropertySymbols(value).length > 0) throw new Error("Query arguments must not contain symbol keys.");
17424
+ if (ancestors.has(value)) throw new Error("Query arguments must not contain cycles.");
17425
+ ancestors.add(value);
17426
+ try {
17427
+ if (Array.isArray(value)) {
17428
+ if (Object.getOwnPropertyNames(value).some((key) => key !== "length" && (!/^(0|[1-9]\\d*)$/.test(key) || Number(key) >= value.length))) {
17429
+ throw new Error("Query arguments must not contain non-index array properties.");
17430
+ }
17431
+ const copy2 = [];
17432
+ for (let index = 0; index < value.length; index += 1) {
17433
+ if (!Object.hasOwn(value, index)) throw new Error("Query arguments must not contain sparse arrays.");
17434
+ copy2.push(normalizeQueryArgumentValue(value[index], ancestors));
17435
+ }
17436
+ return Object.freeze(copy2);
17437
+ }
17438
+ const prototype = Object.getPrototypeOf(value);
17439
+ if (prototype !== Object.prototype && prototype !== null) {
17440
+ throw new Error("Query arguments must contain plain objects only.");
17441
+ }
17442
+ const copy = /* @__PURE__ */ Object.create(null);
17443
+ for (const key of Object.getOwnPropertyNames(value).sort()) {
17444
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
17445
+ if (!descriptor || !descriptor.enumerable || !("value" in descriptor)) throw new Error("Query arguments must not contain accessors.");
17446
+ Object.defineProperty(copy, key, {
17447
+ value: normalizeQueryArgumentValue(descriptor.value, ancestors),
17448
+ enumerable: true,
17449
+ writable: false,
17450
+ configurable: false
17451
+ });
17452
+ }
17453
+ return Object.freeze(copy);
17454
+ } finally {
17455
+ ancestors.delete(value);
17456
+ }
17457
+ }
17299
17458
  async function runMutation(database, auth, mutationName, args) {
17300
17459
  let context;
17301
17460
  let result;
@@ -23645,7 +23804,7 @@ jobs:
23645
23804
  }
23646
23805
 
23647
23806
  // src/cli/cli-version.ts
23648
- var CLI_VERSION = "0.8.3";
23807
+ var CLI_VERSION = "0.8.5";
23649
23808
 
23650
23809
  // src/cli/sporades.ts
23651
23810
  var SUPPORTED_TEMPLATES = new Set(CLIENT_TEMPLATES);
@@ -1,2 +1,2 @@
1
- export declare const CLI_VERSION = "0.8.3";
1
+ export declare const CLI_VERSION = "0.8.5";
2
2
  //# sourceMappingURL=cli-version.d.ts.map
@@ -1,3 +1,3 @@
1
1
  // Generated by `npm run build` from package.json. Do not edit by hand.
2
- export const CLI_VERSION = "0.8.3";
2
+ export const CLI_VERSION = "0.8.5";
3
3
  //# sourceMappingURL=cli-version.js.map
@@ -144,7 +144,7 @@ export declare function sendEmailPasswordResetLink(database: LooseRecord, sessio
144
144
  ok: boolean;
145
145
  error?: undefined;
146
146
  }>;
147
- export declare function runQuery(database: LooseRecord, auth: any, queryName: string): Promise<any>;
147
+ export declare function runQuery(database: LooseRecord, auth: any, queryName: string, rawArgs?: unknown): Promise<any>;
148
148
  export declare function runMutation(database: LooseRecord, auth: any, mutationName: string, args: any): Promise<any>;
149
149
  export declare function runAppMessage(database: LooseRecord, auth: any, messageName: any, data: any, options?: LooseRecord): Promise<any>;
150
150
  export declare function runCurrentUserJobWorker(database: LooseRecord): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"server-runtime-source.d.ts","sourceRoot":"","sources":["../src/server-runtime-source.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"server-runtime-source.d.ts","sourceRoot":"","sources":["../src/server-runtime-source.ts"],"names":[],"mappings":"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"}
@@ -2548,7 +2548,31 @@ export function createWebSocketHub(getDatabase, trustedRefresh = null) {
2548
2548
  });
2549
2549
  return;
2550
2550
  }
2551
- const subscription = { id: message.id, name: queryName, style: message.query ? "direct" : "rows", generation: 0 };
2551
+ let args;
2552
+ try {
2553
+ args = normalizeQueryArguments(message.args ?? []);
2554
+ }
2555
+ catch {
2556
+ sendJson(client, {
2557
+ id: message.id,
2558
+ type: "query.result",
2559
+ query: queryName,
2560
+ data: null,
2561
+ error: invalidQueryArgumentsError(),
2562
+ });
2563
+ return;
2564
+ }
2565
+ if (!message.query && args.length > 0) {
2566
+ sendJson(client, {
2567
+ id: message.id,
2568
+ type: "query.result",
2569
+ query: queryName,
2570
+ data: null,
2571
+ error: invalidQueryArgumentsError(),
2572
+ });
2573
+ return;
2574
+ }
2575
+ const subscription = { id: message.id, name: queryName, args, style: message.query ? "direct" : "rows", generation: 0 };
2552
2576
  client.subscriptions.set(message.id, subscription);
2553
2577
  void sendQueryResult(client, subscription, (error) => sendUnhandledMessageError(client, rawMessage, error));
2554
2578
  return;
@@ -2959,7 +2983,7 @@ export function createWebSocketHub(getDatabase, trustedRefresh = null) {
2959
2983
  subscription.generation = generation;
2960
2984
  try {
2961
2985
  const database = getDatabase();
2962
- const result = await runQuery(database, client.session.auth, subscription.name);
2986
+ const result = await runQuery(database, client.session.auth, subscription.name, subscription.args);
2963
2987
  const data = subscription.style === "direct"
2964
2988
  ? (result.data ?? result.rows)
2965
2989
  : { rows: result.data ?? result.rows };
@@ -3197,7 +3221,14 @@ function sendJsonWithCompletion(client, message, timeoutMs = 250) {
3197
3221
  }
3198
3222
  });
3199
3223
  }
3200
- export async function runQuery(database, auth, queryName) {
3224
+ export async function runQuery(database, auth, queryName, rawArgs = []) {
3225
+ let args;
3226
+ try {
3227
+ args = normalizeQueryArguments(rawArgs);
3228
+ }
3229
+ catch {
3230
+ return { rows: null, data: null, error: invalidQueryArgumentsError() };
3231
+ }
3201
3232
  let context;
3202
3233
  try {
3203
3234
  context = await applyContextMiddleware(database, createMutationContext(database, auth), "query");
@@ -3212,9 +3243,11 @@ export async function runQuery(database, auth, queryName) {
3212
3243
  };
3213
3244
  }
3214
3245
  if (queryName === "ctx.env") {
3246
+ if (args.length > 0)
3247
+ return { rows: null, data: null, error: invalidQueryArgumentsError() };
3215
3248
  return { data: context.env, error: null };
3216
3249
  }
3217
- const customResult = await runCustomQuery(database, context, queryName);
3250
+ const customResult = await runCustomQuery(database, context, queryName, args);
3218
3251
  if (customResult) {
3219
3252
  return customResult;
3220
3253
  }
@@ -3228,6 +3261,8 @@ export async function runQuery(database, auth, queryName) {
3228
3261
  },
3229
3262
  };
3230
3263
  }
3264
+ if (args.length > 0)
3265
+ return { rows: null, data: null, error: invalidQueryArgumentsError() };
3231
3266
  const cacheKey = `${table.name}:${context.auth.userId}`;
3232
3267
  if (!database.rowCache.has(cacheKey)) {
3233
3268
  const columns = ["id", "createdAt", "updatedAt", ...table.fields.map((field) => field.name)];
@@ -3242,7 +3277,7 @@ export async function runQuery(database, auth, queryName) {
3242
3277
  const rows = await filterRowsByReadAcl(database, table, database.rowCache.get(cacheKey), context);
3243
3278
  return { rows, error: null };
3244
3279
  }
3245
- async function runCustomQuery(database, context, queryName) {
3280
+ async function runCustomQuery(database, context, queryName, args) {
3246
3281
  const handler = database.queries.find((candidate) => candidate.name === queryName);
3247
3282
  if (!handler) {
3248
3283
  return null;
@@ -3251,7 +3286,7 @@ async function runCustomQuery(database, context, queryName) {
3251
3286
  const queryHandler = typeof handler.handler === "function"
3252
3287
  ? handler.handler
3253
3288
  : new Function(`return (${handler.handlerSource});`)();
3254
- const data = await queryHandler(context);
3289
+ const data = await queryHandler(context, ...args);
3255
3290
  assertJsonCompatible(data);
3256
3291
  return { data, error: null };
3257
3292
  }
@@ -3269,6 +3304,73 @@ async function runCustomQuery(database, context, queryName) {
3269
3304
  };
3270
3305
  }
3271
3306
  }
3307
+ const QUERY_ARGUMENT_LIMIT_BYTES = 65536;
3308
+ function invalidQueryArgumentsError() {
3309
+ return {
3310
+ message: "Invalid query arguments.",
3311
+ hint: "Use a JSON-compatible argument array no larger than 65536 UTF-8 bytes.",
3312
+ };
3313
+ }
3314
+ function normalizeQueryArguments(value) {
3315
+ if (!Array.isArray(value))
3316
+ throw new Error("Query arguments must be an array.");
3317
+ const snapshot = normalizeQueryArgumentValue(value, new Set());
3318
+ const canonicalJson = JSON.stringify(snapshot);
3319
+ if (Buffer.byteLength(canonicalJson, "utf8") > QUERY_ARGUMENT_LIMIT_BYTES) {
3320
+ throw new Error("Query arguments are too large.");
3321
+ }
3322
+ return snapshot;
3323
+ }
3324
+ function normalizeQueryArgumentValue(value, ancestors) {
3325
+ if (value === null || typeof value === "string" || typeof value === "boolean")
3326
+ return value;
3327
+ if (typeof value === "number") {
3328
+ if (!Number.isFinite(value))
3329
+ throw new Error("Query arguments must contain finite numbers.");
3330
+ return value;
3331
+ }
3332
+ if (typeof value !== "object")
3333
+ throw new Error("Query arguments must be JSON-compatible.");
3334
+ if (Object.getOwnPropertySymbols(value).length > 0)
3335
+ throw new Error("Query arguments must not contain symbol keys.");
3336
+ if (ancestors.has(value))
3337
+ throw new Error("Query arguments must not contain cycles.");
3338
+ ancestors.add(value);
3339
+ try {
3340
+ if (Array.isArray(value)) {
3341
+ if (Object.getOwnPropertyNames(value).some((key) => key !== "length" && (!/^(0|[1-9]\\d*)$/.test(key) || Number(key) >= value.length))) {
3342
+ throw new Error("Query arguments must not contain non-index array properties.");
3343
+ }
3344
+ const copy = [];
3345
+ for (let index = 0; index < value.length; index += 1) {
3346
+ if (!Object.hasOwn(value, index))
3347
+ throw new Error("Query arguments must not contain sparse arrays.");
3348
+ copy.push(normalizeQueryArgumentValue(value[index], ancestors));
3349
+ }
3350
+ return Object.freeze(copy);
3351
+ }
3352
+ const prototype = Object.getPrototypeOf(value);
3353
+ if (prototype !== Object.prototype && prototype !== null) {
3354
+ throw new Error("Query arguments must contain plain objects only.");
3355
+ }
3356
+ const copy = Object.create(null);
3357
+ for (const key of Object.getOwnPropertyNames(value).sort()) {
3358
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
3359
+ if (!descriptor || !descriptor.enumerable || !("value" in descriptor))
3360
+ throw new Error("Query arguments must not contain accessors.");
3361
+ Object.defineProperty(copy, key, {
3362
+ value: normalizeQueryArgumentValue(descriptor.value, ancestors),
3363
+ enumerable: true,
3364
+ writable: false,
3365
+ configurable: false,
3366
+ });
3367
+ }
3368
+ return Object.freeze(copy);
3369
+ }
3370
+ finally {
3371
+ ancestors.delete(value);
3372
+ }
3373
+ }
3272
3374
  export async function runMutation(database, auth, mutationName, args) {
3273
3375
  let context;
3274
3376
  let result;