@softr/sdk 0.0.11 → 0.0.12

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.
@@ -2,6 +2,533 @@ import { getRequestHeader, getRequestProtocol, getResponseHeaders, setResponseHe
2
2
  import { z } from "zod";
3
3
  import { createCipheriv, createDecipheriv, createHash, randomBytes } from "node:crypto";
4
4
 
5
+ //#region src/core/query.ts
6
+ const INTERNAL_KIND = Symbol("KIND");
7
+ /**
8
+ * Query builder for filter queries. This exist to alleviate the (LLM?) mental overhead. API filter schema is a bit
9
+ * verbose and confusing, even when dealing with very basic queries. So to fall into the pit of success, aim here
10
+ * is to simplify filtering as much as possible.
11
+ *
12
+ * @example
13
+ * single filter
14
+ * ```tsx
15
+ * const query = q.text("title").contains("hello");
16
+ *
17
+ * const json = buildFilterCriteria(query);
18
+ * // result
19
+ * {
20
+ * "filterCriteria": {
21
+ * "filterConditionGroup": {
22
+ * "logicalOperator": "AND"
23
+ * "filterGroups": [{
24
+ * "logicalOperator": "AND",
25
+ * "filters": [{
26
+ * "subject": "title",
27
+ * "type": "TEXT",
28
+ * "operator": "CONTAINS",
29
+ * "value": "hello"
30
+ * }],
31
+ * }]
32
+ * }
33
+ * }
34
+ * }
35
+ *
36
+ * ```
37
+ *
38
+ * @example
39
+ * combining multiple filters
40
+ * ```tsx
41
+ * const query = q.and(
42
+ * q.text("title").contains("hello"),
43
+ * q.number("views").is(100),
44
+ * q.or(
45
+ * q.text("author").is("John"),
46
+ * q.boolean("isPublished").is(true)
47
+ * )
48
+ * );
49
+ * ```
50
+ */
51
+ const q = {
52
+ select(mapping) {
53
+ return mapping;
54
+ },
55
+ and(...filters) {
56
+ return {
57
+ logicalOperator: "AND",
58
+ filters,
59
+ [INTERNAL_KIND]: "LOGICAL_GROUP"
60
+ };
61
+ },
62
+ or(...filters) {
63
+ return {
64
+ logicalOperator: "OR",
65
+ filters,
66
+ [INTERNAL_KIND]: "LOGICAL_GROUP"
67
+ };
68
+ },
69
+ text(subject) {
70
+ const filter = {
71
+ subject,
72
+ type: "TEXT",
73
+ [INTERNAL_KIND]: "FILTER"
74
+ };
75
+ return {
76
+ is(value) {
77
+ return {
78
+ ...filter,
79
+ operator: "IS",
80
+ value
81
+ };
82
+ },
83
+ isNot(value) {
84
+ return {
85
+ ...filter,
86
+ operator: "IS_NOT",
87
+ value
88
+ };
89
+ },
90
+ contains(value) {
91
+ return {
92
+ ...filter,
93
+ operator: "CONTAINS",
94
+ value
95
+ };
96
+ },
97
+ isOneOf(value) {
98
+ return {
99
+ ...filter,
100
+ operator: "IS_ONE_OF",
101
+ value
102
+ };
103
+ },
104
+ isNoneOf(value) {
105
+ return {
106
+ ...filter,
107
+ operator: "IS_NONE_OF",
108
+ value
109
+ };
110
+ },
111
+ startsWith(value) {
112
+ return {
113
+ ...filter,
114
+ operator: "STARTS_WITH",
115
+ value
116
+ };
117
+ },
118
+ endsWith(value) {
119
+ return {
120
+ ...filter,
121
+ operator: "ENDS_WITH",
122
+ value
123
+ };
124
+ },
125
+ hasAllOf(value) {
126
+ return {
127
+ ...filter,
128
+ operator: "HAS_ALL_OF",
129
+ value
130
+ };
131
+ },
132
+ isEmpty() {
133
+ return {
134
+ ...filter,
135
+ operator: "IS_EMPTY"
136
+ };
137
+ },
138
+ isNotEmpty() {
139
+ return {
140
+ ...filter,
141
+ operator: "IS_NOT_EMPTY"
142
+ };
143
+ }
144
+ };
145
+ },
146
+ boolean(subject) {
147
+ const filter = {
148
+ subject,
149
+ type: "BOOLEAN",
150
+ [INTERNAL_KIND]: "FILTER"
151
+ };
152
+ return {
153
+ is(value) {
154
+ return {
155
+ ...filter,
156
+ operator: "IS",
157
+ value
158
+ };
159
+ },
160
+ isNot(value) {
161
+ return {
162
+ ...filter,
163
+ operator: "IS_NOT",
164
+ value
165
+ };
166
+ },
167
+ isEmpty() {
168
+ return {
169
+ ...filter,
170
+ operator: "IS_EMPTY"
171
+ };
172
+ },
173
+ isNotEmpty() {
174
+ return {
175
+ ...filter,
176
+ operator: "IS_NOT_EMPTY"
177
+ };
178
+ }
179
+ };
180
+ },
181
+ number(subject) {
182
+ const filter = {
183
+ subject,
184
+ type: "NUMERIC",
185
+ [INTERNAL_KIND]: "FILTER"
186
+ };
187
+ return {
188
+ is(value) {
189
+ return {
190
+ ...filter,
191
+ operator: "IS",
192
+ value: value.toString()
193
+ };
194
+ },
195
+ isNot(value) {
196
+ return {
197
+ ...filter,
198
+ operator: "IS_NOT",
199
+ value: value.toString()
200
+ };
201
+ },
202
+ gt(value) {
203
+ return {
204
+ ...filter,
205
+ operator: "GT",
206
+ value: value.toString()
207
+ };
208
+ },
209
+ gte(value) {
210
+ return {
211
+ ...filter,
212
+ operator: "GTE",
213
+ value: value.toString()
214
+ };
215
+ },
216
+ lt(value) {
217
+ return {
218
+ ...filter,
219
+ operator: "LT",
220
+ value: value.toString()
221
+ };
222
+ },
223
+ lte(value) {
224
+ return {
225
+ ...filter,
226
+ operator: "LTE",
227
+ value: value.toString()
228
+ };
229
+ },
230
+ between(start, end) {
231
+ return {
232
+ ...filter,
233
+ operator: "BETWEEN",
234
+ value: [start.toString(), end.toString()]
235
+ };
236
+ },
237
+ isEmpty() {
238
+ return {
239
+ ...filter,
240
+ operator: "IS_EMPTY"
241
+ };
242
+ },
243
+ isNotEmpty() {
244
+ return {
245
+ ...filter,
246
+ operator: "IS_NOT_EMPTY"
247
+ };
248
+ }
249
+ };
250
+ },
251
+ array(subject) {
252
+ const filter = {
253
+ subject,
254
+ type: "ARRAY",
255
+ [INTERNAL_KIND]: "FILTER"
256
+ };
257
+ return {
258
+ is(value) {
259
+ return {
260
+ ...filter,
261
+ operator: "IS",
262
+ value
263
+ };
264
+ },
265
+ isOneOf(value) {
266
+ return {
267
+ ...filter,
268
+ operator: "IS_ONE_OF",
269
+ value
270
+ };
271
+ },
272
+ isNoneOf(value) {
273
+ return {
274
+ ...filter,
275
+ operator: "IS_NONE_OF",
276
+ value
277
+ };
278
+ },
279
+ hasAllOf(value) {
280
+ return {
281
+ ...filter,
282
+ operator: "HAS_ALL_OF",
283
+ value
284
+ };
285
+ },
286
+ isEmpty() {
287
+ return {
288
+ ...filter,
289
+ operator: "IS_EMPTY"
290
+ };
291
+ },
292
+ isNotEmpty() {
293
+ return {
294
+ ...filter,
295
+ operator: "IS_NOT_EMPTY"
296
+ };
297
+ }
298
+ };
299
+ },
300
+ date(subject) {
301
+ const filter = {
302
+ subject,
303
+ type: "DATE",
304
+ [INTERNAL_KIND]: "FILTER"
305
+ };
306
+ return {
307
+ is(value) {
308
+ return {
309
+ ...filter,
310
+ operator: "IS",
311
+ value
312
+ };
313
+ },
314
+ isNot(value) {
315
+ return {
316
+ ...filter,
317
+ operator: "IS_NOT",
318
+ value
319
+ };
320
+ },
321
+ gt(value) {
322
+ return {
323
+ ...filter,
324
+ operator: "GT",
325
+ value
326
+ };
327
+ },
328
+ gte(value) {
329
+ return {
330
+ ...filter,
331
+ operator: "GTE",
332
+ value
333
+ };
334
+ },
335
+ lt(value) {
336
+ return {
337
+ ...filter,
338
+ operator: "LT",
339
+ value
340
+ };
341
+ },
342
+ lte(value) {
343
+ return {
344
+ ...filter,
345
+ operator: "LTE",
346
+ value
347
+ };
348
+ },
349
+ between(start, end) {
350
+ return {
351
+ ...filter,
352
+ operator: "BETWEEN",
353
+ value: [start, end]
354
+ };
355
+ },
356
+ isNotBetween(start, end) {
357
+ return {
358
+ ...filter,
359
+ operator: "IS_NOT_BETWEEN",
360
+ value: [start, end]
361
+ };
362
+ },
363
+ isEmpty() {
364
+ return {
365
+ ...filter,
366
+ operator: "IS_EMPTY"
367
+ };
368
+ },
369
+ isNotEmpty() {
370
+ return {
371
+ ...filter,
372
+ operator: "IS_NOT_EMPTY"
373
+ };
374
+ }
375
+ };
376
+ },
377
+ asc(subject) {
378
+ return {
379
+ sortingField: subject,
380
+ sortType: "ASC",
381
+ [INTERNAL_KIND]: "ORDER_BY"
382
+ };
383
+ },
384
+ desc(subject) {
385
+ return {
386
+ sortingField: subject,
387
+ sortType: "DESC",
388
+ [INTERNAL_KIND]: "ORDER_BY"
389
+ };
390
+ }
391
+ };
392
+ /**
393
+ * Converts filter utility conditions to a format our backend supports
394
+ *
395
+ * @example
396
+ * ```tsx
397
+ * const query = q.text("title").contains("hello");
398
+ *
399
+ * const json = buildFilterCriteria(query);
400
+ * // result
401
+ * {
402
+ * "filterCriteria": {
403
+ * "filterConditionGroup": {
404
+ * "logicalOperator": "AND"
405
+ * "filterGroups": [{
406
+ * "logicalOperator": "AND",
407
+ * "filters": [{
408
+ * "subject": "title",
409
+ * "type": "TEXT",
410
+ * "operator": "CONTAINS",
411
+ * "value": "hello"
412
+ * }],
413
+ * }]
414
+ * }
415
+ * }
416
+ * }
417
+ *
418
+ * ```
419
+ */
420
+ function buildFilterCriteria(input) {
421
+ if (!(INTERNAL_KIND in input)) throw new Error("Filters cannot be constructed manually. Use the 'q' helper.");
422
+ if (isFilter(input)) return { filterConditionGroup: {
423
+ logicalOperator: "AND",
424
+ filterGroups: [{
425
+ logicalOperator: "AND",
426
+ filters: [stripInternalKind(input)]
427
+ }]
428
+ } };
429
+ if (isFilterGroup(input)) {
430
+ if (input.filters.length === 0) return { filterConditionGroup: {
431
+ logicalOperator: input.logicalOperator,
432
+ filterGroups: []
433
+ } };
434
+ return { filterConditionGroup: {
435
+ logicalOperator: input.logicalOperator,
436
+ filterGroups: input.filters.map((inner) => {
437
+ if (isFilter(inner)) return {
438
+ logicalOperator: "AND",
439
+ filters: [stripInternalKind(inner)]
440
+ };
441
+ if (!inner.filters.every(isFilter)) throw new Error("Cannot nest filter groups more than three levels deep.");
442
+ return {
443
+ logicalOperator: inner.logicalOperator,
444
+ filters: inner.filters.map(stripInternalKind)
445
+ };
446
+ })
447
+ } };
448
+ }
449
+ throw new Error("Unknown filter kind: " + input[INTERNAL_KIND]);
450
+ }
451
+ /**
452
+ * Converts order-by utility conditions to a format our backend supports
453
+ *
454
+ * @example
455
+ * single orderBy
456
+ * ```tsx
457
+ * const orderBy = buildOrderBy(q.asc("title"));
458
+ * // result: [{ sortingField: "title", sortType: "ASC" }]
459
+ * ```
460
+ *
461
+ * @example
462
+ * multiple orderBys
463
+ * ```tsx
464
+ * const orderBy = buildOrderBy([q.desc("createdAt"), q.asc("title")]);
465
+ * // result: [
466
+ * // { sortingField: "createdAt", sortType: "DESC" },
467
+ * // { sortingField: "title", sortType: "ASC" }
468
+ * // ]
469
+ * ```
470
+ */
471
+ function buildOrderBy(input) {
472
+ const orderBys = Array.isArray(input) ? input : [input];
473
+ if (!orderBys.every((ob) => ob[INTERNAL_KIND] === "ORDER_BY")) throw new Error("orderBy conditions cannot be constructed manually. Use the 'q' helper.");
474
+ return orderBys.map((orderBy) => ({
475
+ sortingField: orderBy.sortingField,
476
+ sortType: orderBy.sortType
477
+ }));
478
+ }
479
+ function dealiasOrderBy(orderbys, aliases) {
480
+ return orderbys.map((orderBy) => {
481
+ const subject = aliases[orderBy.sortingField];
482
+ if (!subject) throw new Error(`Could not find an alias for subject "${orderBy.sortingField}"`);
483
+ return {
484
+ ...orderBy,
485
+ sortingField: subject
486
+ };
487
+ });
488
+ }
489
+ /**
490
+ * Replaces filter subjects with their aliased values.
491
+ *
492
+ * Throws an error if any filter subject does not have a corresponding alias in the provided mapping.
493
+ *
494
+ * @example
495
+ * ```tsx
496
+ * const criteria = buildFilterCriteria(q.text("name").contains("John"));
497
+ * const aliases = { name: "abcxy123" };
498
+ * const dealiased = dealiasFilterCriteria(criteria, aliases);
499
+ * // Result: filter subject is now "abcxyz123" instead of "name"
500
+ * ```
501
+ */
502
+ function dealiasFilterCriteria(criteria, aliases) {
503
+ if (!criteria.filterConditionGroup) return criteria;
504
+ return { filterConditionGroup: {
505
+ ...criteria.filterConditionGroup,
506
+ filterGroups: criteria.filterConditionGroup.filterGroups.map((group) => ({
507
+ ...group,
508
+ filters: group.filters.map(dealiasFilter)
509
+ }))
510
+ } };
511
+ function dealiasFilter(filter) {
512
+ const subject = aliases[filter.subject];
513
+ if (!subject) throw new Error(`Could not find an alias for subject "${subject}"`);
514
+ return {
515
+ ...filter,
516
+ subject
517
+ };
518
+ }
519
+ }
520
+ function stripInternalKind(item) {
521
+ const { [INTERNAL_KIND]: _,...rest } = item;
522
+ return rest;
523
+ }
524
+ function isFilterGroup(item) {
525
+ return item[INTERNAL_KIND] === "LOGICAL_GROUP";
526
+ }
527
+ function isFilter(item) {
528
+ return item[INTERNAL_KIND] === "FILTER";
529
+ }
530
+
531
+ //#endregion
5
532
  //#region src/assertions.ts
6
533
  function assert(condition, message) {
7
534
  if (!condition) throwAtCaller(assert, new AssertionError(unwrapErrorMessage(message)));
@@ -95,6 +622,10 @@ function serializeCookie(name, value, { maxAge, secure }) {
95
622
  parts.push("SameSite=Lax", `Max-Age=${maxAge}`);
96
623
  return parts.join("; ");
97
624
  }
625
+ const USER_TIMEZONE_COOKIE = "user-timezone";
626
+ function readUserTimezone() {
627
+ return readRequestCookies()[USER_TIMEZONE_COOKIE] ?? null;
628
+ }
98
629
  function writeCookies(cookies) {
99
630
  if (cookies.length === 0) return;
100
631
  setResponseHeader("Set-Cookie", [...getResponseHeaders().getSetCookie(), ...cookies]);
@@ -357,4 +888,4 @@ function openEnvelope(raw, purpose) {
357
888
  }
358
889
 
359
890
  //#endregion
360
- export { readSessionToken as a, writeCookies as c, softAssert as d, todo as f, readSessionData as i, assert as l, makeSessionDataCookie as n, fetchVisitor as o, makeSessionTokenCookie as r, platformFetch as s, clearSession as t, panic as u };
891
+ export { dealiasOrderBy as _, readSessionToken as a, readUserTimezone as c, panic as d, softAssert as f, dealiasFilterCriteria as g, buildOrderBy as h, readSessionData as i, writeCookies as l, buildFilterCriteria as m, makeSessionDataCookie as n, fetchVisitor as o, todo as p, makeSessionTokenCookie as r, platformFetch as s, clearSession as t, assert as u, q as v };