@powersync/service-core 0.3.0 → 0.4.1

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,1053 +0,0 @@
1
- import {
2
- DEFAULT_SCHEMA,
3
- DEFAULT_TAG,
4
- DartSchemaGenerator,
5
- ExpressionType,
6
- JsSchemaGenerator,
7
- SourceTableInterface,
8
- SqlDataQuery,
9
- SqlParameterQuery,
10
- SqlSyncRules,
11
- StaticSchema,
12
- normalizeTokenParameters
13
- } from '@powersync/service-sync-rules';
14
- import { describe, expect, test } from 'vitest';
15
- import { SourceTable } from '../../src/storage/SourceTable.js';
16
-
17
- class TestSourceTable implements SourceTableInterface {
18
- readonly connectionTag = DEFAULT_TAG;
19
- readonly schema = DEFAULT_SCHEMA;
20
-
21
- constructor(public readonly table: string) {}
22
- }
23
-
24
- const ASSETS = new TestSourceTable('assets');
25
- const USERS = new TestSourceTable('users');
26
-
27
- describe('sync rules', () => {
28
- test('parse empty sync rules', () => {
29
- const rules = SqlSyncRules.fromYaml('bucket_definitions: {}');
30
- expect(rules.bucket_descriptors).toEqual([]);
31
- });
32
-
33
- test('parse global sync rules', () => {
34
- const rules = SqlSyncRules.fromYaml(`
35
- bucket_definitions:
36
- mybucket:
37
- data:
38
- - SELECT id, description FROM assets
39
- `);
40
- const bucket = rules.bucket_descriptors[0];
41
- expect(bucket.name).toEqual('mybucket');
42
- expect(bucket.bucket_parameters).toEqual([]);
43
- const dataQuery = bucket.data_queries[0];
44
- expect(dataQuery.bucket_parameters).toEqual([]);
45
- expect(dataQuery.columnOutputNames()).toEqual(['id', 'description']);
46
- expect(rules.evaluateRow({ sourceTable: ASSETS, record: { id: 'asset1', description: 'test' } })).toEqual([
47
- {
48
- ruleId: '1',
49
- table: 'assets',
50
- id: 'asset1',
51
- data: {
52
- id: 'asset1',
53
- description: 'test'
54
- },
55
- bucket: 'mybucket[]'
56
- }
57
- ]);
58
- expect(rules.getStaticBucketIds({ token_parameters: {}, user_parameters: {} })).toEqual(['mybucket[]']);
59
- });
60
-
61
- test('parse global sync rules with filter', () => {
62
- const rules = SqlSyncRules.fromYaml(`
63
- bucket_definitions:
64
- mybucket:
65
- parameters: SELECT WHERE token_parameters.is_admin
66
- data: []
67
- `);
68
- const bucket = rules.bucket_descriptors[0];
69
- expect(bucket.bucket_parameters).toEqual([]);
70
- const param_query = bucket.global_parameter_queries[0];
71
-
72
- expect(param_query.filter!.filter({ token_parameters: { is_admin: 1n } })).toEqual([{}]);
73
- expect(param_query.filter!.filter({ token_parameters: { is_admin: 0n } })).toEqual([]);
74
- expect(rules.getStaticBucketIds(normalizeTokenParameters({ is_admin: true }))).toEqual(['mybucket[]']);
75
- expect(rules.getStaticBucketIds(normalizeTokenParameters({ is_admin: false }))).toEqual([]);
76
- expect(rules.getStaticBucketIds(normalizeTokenParameters({}))).toEqual([]);
77
- });
78
-
79
- test('parse global sync rules with table filter', () => {
80
- const rules = SqlSyncRules.fromYaml(`
81
- bucket_definitions:
82
- mybucket:
83
- parameters: SELECT FROM users WHERE users.id = token_parameters.user_id AND users.is_admin
84
- data: []
85
- `);
86
- const bucket = rules.bucket_descriptors[0];
87
- expect(bucket.bucket_parameters).toEqual([]);
88
- const param_query = bucket.parameter_queries[0];
89
- expect(param_query.bucket_parameters).toEqual([]);
90
- expect(rules.evaluateParameterRow(USERS, { id: 'user1', is_admin: 1 })).toEqual([
91
- {
92
- bucket_parameters: [{}],
93
- lookup: ['mybucket', '1', 'user1']
94
- }
95
- ]);
96
- expect(rules.evaluateParameterRow(USERS, { id: 'user1', is_admin: 0 })).toEqual([]);
97
- });
98
-
99
- test('parse bucket with parameters', () => {
100
- const rules = SqlSyncRules.fromYaml(`
101
- bucket_definitions:
102
- mybucket:
103
- parameters: SELECT token_parameters.user_id, user_parameters.device_id
104
- data:
105
- - SELECT id, description FROM assets WHERE assets.user_id = bucket.user_id AND assets.device_id = bucket.device_id AND NOT assets.archived
106
- `);
107
- const bucket = rules.bucket_descriptors[0];
108
- expect(bucket.bucket_parameters).toEqual(['user_id', 'device_id']);
109
- const param_query = bucket.global_parameter_queries[0];
110
- expect(param_query.bucket_parameters).toEqual(['user_id', 'device_id']);
111
- expect(rules.getStaticBucketIds(normalizeTokenParameters({ user_id: 'user1' }, { device_id: 'device1' }))).toEqual([
112
- 'mybucket["user1","device1"]'
113
- ]);
114
-
115
- const data_query = bucket.data_queries[0];
116
- expect(data_query.bucket_parameters).toEqual(['user_id', 'device_id']);
117
- expect(
118
- rules.evaluateRow({
119
- sourceTable: ASSETS,
120
- record: { id: 'asset1', description: 'test', user_id: 'user1', device_id: 'device1' }
121
- })
122
- ).toEqual([
123
- {
124
- ruleId: '1',
125
- bucket: 'mybucket["user1","device1"]',
126
- id: 'asset1',
127
- data: {
128
- id: 'asset1',
129
- description: 'test'
130
- },
131
- table: 'assets'
132
- }
133
- ]);
134
- expect(
135
- rules.evaluateRow({
136
- sourceTable: ASSETS,
137
- record: { id: 'asset1', description: 'test', user_id: 'user1', archived: 1, device_id: 'device1' }
138
- })
139
- ).toEqual([]);
140
- });
141
-
142
- test('parse bucket with parameters and OR condition', () => {
143
- const rules = SqlSyncRules.fromYaml(`
144
- bucket_definitions:
145
- mybucket:
146
- parameters: SELECT token_parameters.user_id
147
- data:
148
- - SELECT id, description FROM assets WHERE assets.user_id = bucket.user_id OR assets.owner_id = bucket.user_id
149
- `);
150
- const bucket = rules.bucket_descriptors[0];
151
- expect(bucket.bucket_parameters).toEqual(['user_id']);
152
- const param_query = bucket.global_parameter_queries[0];
153
- expect(param_query.bucket_parameters).toEqual(['user_id']);
154
- expect(rules.getStaticBucketIds(normalizeTokenParameters({ user_id: 'user1' }))).toEqual(['mybucket["user1"]']);
155
-
156
- const data_query = bucket.data_queries[0];
157
- expect(data_query.bucket_parameters).toEqual(['user_id']);
158
- expect(
159
- rules.evaluateRow({
160
- sourceTable: ASSETS,
161
- record: { id: 'asset1', description: 'test', user_id: 'user1' }
162
- })
163
- ).toEqual([
164
- {
165
- ruleId: '1',
166
- bucket: 'mybucket["user1"]',
167
- id: 'asset1',
168
- data: {
169
- id: 'asset1',
170
- description: 'test'
171
- },
172
- table: 'assets'
173
- }
174
- ]);
175
- expect(
176
- rules.evaluateRow({
177
- sourceTable: ASSETS,
178
- record: { id: 'asset1', description: 'test', owner_id: 'user1' }
179
- })
180
- ).toEqual([
181
- {
182
- ruleId: '1',
183
- bucket: 'mybucket["user1"]',
184
- id: 'asset1',
185
- data: {
186
- id: 'asset1',
187
- description: 'test'
188
- },
189
- table: 'assets'
190
- }
191
- ]);
192
- });
193
-
194
- test('parse bucket with parameters and invalid OR condition', () => {
195
- expect(() => {
196
- const rules = SqlSyncRules.fromYaml(`
197
- bucket_definitions:
198
- mybucket:
199
- parameters: SELECT token_parameters.user_id
200
- data:
201
- - SELECT id, description FROM assets WHERE assets.user_id = bucket.user_id AND (assets.user_id = bucket.foo OR assets.other_id = bucket.bar)
202
- `);
203
- }).toThrowError(/must use the same parameters/);
204
- });
205
-
206
- test('reject unsupported queries', () => {
207
- expect(
208
- SqlSyncRules.validate(`
209
- bucket_definitions:
210
- mybucket:
211
- parameters: SELECT token_parameters.user_id LIMIT 1
212
- data: []
213
- `)
214
- ).toMatchObject([{ message: 'LIMIT is not supported' }]);
215
-
216
- expect(
217
- SqlSyncRules.validate(`
218
- bucket_definitions:
219
- mybucket:
220
- data:
221
- - SELECT DISTINCT id, description FROM assets
222
- `)
223
- ).toMatchObject([{ message: 'DISTINCT is not supported' }]);
224
-
225
- expect(
226
- SqlSyncRules.validate(`
227
- bucket_definitions:
228
- mybucket:
229
- parameters: SELECT token_parameters.user_id OFFSET 10
230
- data: []
231
- `)
232
- ).toMatchObject([{ message: 'LIMIT is not supported' }]);
233
-
234
- expect(() => {
235
- const rules = SqlSyncRules.fromYaml(`
236
- bucket_definitions:
237
- mybucket:
238
- parameters: SELECT token_parameters.user_id FOR UPDATE SKIP LOCKED
239
- data: []
240
- `);
241
- }).toThrowError(/SKIP is not supported/);
242
-
243
- expect(() => {
244
- const rules = SqlSyncRules.fromYaml(`
245
- bucket_definitions:
246
- mybucket:
247
- parameters: SELECT token_parameters.user_id FOR UPDATE
248
- data: []
249
- `);
250
- }).toThrowError(/FOR is not supported/);
251
-
252
- expect(() => {
253
- const rules = SqlSyncRules.fromYaml(`
254
- bucket_definitions:
255
- mybucket:
256
- data:
257
- - SELECT id, description FROM assets ORDER BY id
258
- `);
259
- }).toThrowError(/ORDER BY is not supported/);
260
- });
261
-
262
- test('transforming things', () => {
263
- const rules = SqlSyncRules.fromYaml(`
264
- bucket_definitions:
265
- mybucket:
266
- parameters: SELECT upper(token_parameters.user_id) AS user_id
267
- data:
268
- - SELECT id, upper(description) AS description_upper FROM assets WHERE upper(assets.user_id) = bucket.user_id AND NOT assets.archived
269
- `);
270
- const bucket = rules.bucket_descriptors[0];
271
- expect(bucket.bucket_parameters).toEqual(['user_id']);
272
- expect(rules.getStaticBucketIds(normalizeTokenParameters({ user_id: 'user1' }))).toEqual(['mybucket["USER1"]']);
273
-
274
- expect(
275
- rules.evaluateRow({
276
- sourceTable: ASSETS,
277
- record: { id: 'asset1', description: 'test', user_id: 'user1' }
278
- })
279
- ).toEqual([
280
- {
281
- ruleId: '1',
282
- bucket: 'mybucket["USER1"]',
283
- id: 'asset1',
284
- data: {
285
- id: 'asset1',
286
- description_upper: 'TEST'
287
- },
288
- table: 'assets'
289
- }
290
- ]);
291
- });
292
-
293
- test('transforming things with upper-case functions', () => {
294
- // Testing that we can use different case for the function names
295
- const rules = SqlSyncRules.fromYaml(`
296
- bucket_definitions:
297
- mybucket:
298
- parameters: SELECT UPPER(token_parameters.user_id) AS user_id
299
- data:
300
- - SELECT id, UPPER(description) AS description_upper FROM assets WHERE UPPER(assets.user_id) = bucket.user_id AND NOT assets.archived
301
- `);
302
- const bucket = rules.bucket_descriptors[0];
303
- expect(bucket.bucket_parameters).toEqual(['user_id']);
304
- expect(rules.getStaticBucketIds(normalizeTokenParameters({ user_id: 'user1' }))).toEqual(['mybucket["USER1"]']);
305
-
306
- expect(
307
- rules.evaluateRow({
308
- sourceTable: ASSETS,
309
- record: { id: 'asset1', description: 'test', user_id: 'user1' }
310
- })
311
- ).toEqual([
312
- {
313
- ruleId: '1',
314
- bucket: 'mybucket["USER1"]',
315
- id: 'asset1',
316
- data: {
317
- id: 'asset1',
318
- description_upper: 'TEST'
319
- },
320
- table: 'assets'
321
- }
322
- ]);
323
- });
324
-
325
- test('transforming json', () => {
326
- const rules = SqlSyncRules.fromYaml(`
327
- bucket_definitions:
328
- mybucket:
329
- data:
330
- - SELECT id, data ->> 'count' AS count, data -> 'bool' AS bool1, data ->> 'bool' AS bool2, 'true' ->> '$' as bool3, json_extract(data, '$.bool') AS bool4 FROM assets
331
- `);
332
- expect(
333
- rules.evaluateRow({
334
- sourceTable: ASSETS,
335
- record: { id: 'asset1', data: JSON.stringify({ count: 5, bool: true }) }
336
- })
337
- ).toEqual([
338
- {
339
- ruleId: '1',
340
- bucket: 'mybucket[]',
341
- id: 'asset1',
342
- data: {
343
- id: 'asset1',
344
- count: 5n,
345
- bool1: 'true',
346
- bool2: 1n,
347
- bool3: 1n,
348
- bool4: 1n
349
- },
350
- table: 'assets'
351
- }
352
- ]);
353
- });
354
-
355
- test('IN json', () => {
356
- const rules = SqlSyncRules.fromYaml(`
357
- bucket_definitions:
358
- mybucket:
359
- parameters: SELECT token_parameters.region_id
360
- data:
361
- - SELECT id, description FROM assets WHERE bucket.region_id IN assets.region_ids
362
- `);
363
-
364
- expect(
365
- rules.evaluateRow({
366
- sourceTable: ASSETS,
367
- record: {
368
- id: 'asset1',
369
- description: 'test',
370
- region_ids: JSON.stringify(['region1', 'region2'])
371
- }
372
- })
373
- ).toEqual([
374
- {
375
- ruleId: '1',
376
- bucket: 'mybucket["region1"]',
377
- id: 'asset1',
378
- data: {
379
- id: 'asset1',
380
- description: 'test'
381
- },
382
- table: 'assets'
383
- },
384
- {
385
- ruleId: '1',
386
- bucket: 'mybucket["region2"]',
387
- id: 'asset1',
388
- data: {
389
- id: 'asset1',
390
- description: 'test'
391
- },
392
- table: 'assets'
393
- }
394
- ]);
395
- });
396
-
397
- test('direct boolean param', () => {
398
- const rules = SqlSyncRules.fromYaml(`
399
- bucket_definitions:
400
- mybucket:
401
- parameters: SELECT token_parameters.is_admin
402
- data:
403
- - SELECT id, description, role, 'admin' as rule FROM assets WHERE bucket.is_admin
404
- - SELECT id, description, role, 'normal' as rule FROM assets WHERE (bucket.is_admin OR bucket.is_admin = false) AND assets.role != 'admin'
405
- `);
406
-
407
- expect(
408
- rules.evaluateRow({ sourceTable: ASSETS, record: { id: 'asset1', description: 'test', role: 'admin' } })
409
- ).toEqual([
410
- {
411
- ruleId: '1',
412
- bucket: 'mybucket[1]',
413
- id: 'asset1',
414
- data: {
415
- id: 'asset1',
416
- description: 'test',
417
- role: 'admin',
418
- rule: 'admin'
419
- },
420
- table: 'assets'
421
- }
422
- ]);
423
-
424
- // TODO: Deduplicate somewhere
425
- expect(
426
- rules.evaluateRow({ sourceTable: ASSETS, record: { id: 'asset2', description: 'test', role: 'normal' } })
427
- ).toEqual([
428
- {
429
- ruleId: '1',
430
- bucket: 'mybucket[1]',
431
- id: 'asset2',
432
- data: {
433
- id: 'asset2',
434
- description: 'test',
435
- role: 'normal',
436
- rule: 'admin'
437
- },
438
- table: 'assets'
439
- },
440
- {
441
- ruleId: '2',
442
- bucket: 'mybucket[1]',
443
- id: 'asset2',
444
- data: {
445
- id: 'asset2',
446
- description: 'test',
447
- role: 'normal',
448
- rule: 'normal'
449
- },
450
- table: 'assets'
451
- },
452
- {
453
- ruleId: '2',
454
- bucket: 'mybucket[0]',
455
- id: 'asset2',
456
- data: {
457
- id: 'asset2',
458
- description: 'test',
459
- role: 'normal',
460
- rule: 'normal'
461
- },
462
- table: 'assets'
463
- }
464
- ]);
465
-
466
- expect(rules.getStaticBucketIds(normalizeTokenParameters({ is_admin: true }))).toEqual(['mybucket[1]']);
467
- });
468
-
469
- test('token_parameters IN query', function () {
470
- const sql = 'SELECT id as group_id FROM groups WHERE token_parameters.user_id IN groups.user_ids';
471
- const query = SqlParameterQuery.fromSql('mybucket', sql) as SqlParameterQuery;
472
- query.id = '1';
473
- expect(query.evaluateParameterRow({ id: 'group1', user_ids: JSON.stringify(['user1', 'user2']) })).toEqual([
474
- {
475
- lookup: ['mybucket', '1', 'user1'],
476
- bucket_parameters: [
477
- {
478
- group_id: 'group1'
479
- }
480
- ]
481
- },
482
- {
483
- lookup: ['mybucket', '1', 'user2'],
484
- bucket_parameters: [
485
- {
486
- group_id: 'group1'
487
- }
488
- ]
489
- }
490
- ]);
491
- expect(
492
- query.getLookups(
493
- normalizeTokenParameters({
494
- user_id: 'user1'
495
- })
496
- )
497
- ).toEqual([['mybucket', '1', 'user1']]);
498
- });
499
-
500
- test('IN token_parameters query', function () {
501
- const sql = 'SELECT id as region_id FROM regions WHERE name IN token_parameters.region_names';
502
- const query = SqlParameterQuery.fromSql('mybucket', sql) as SqlParameterQuery;
503
- query.id = '1';
504
- expect(query.evaluateParameterRow({ id: 'region1', name: 'colorado' })).toEqual([
505
- {
506
- lookup: ['mybucket', '1', 'colorado'],
507
- bucket_parameters: [
508
- {
509
- region_id: 'region1'
510
- }
511
- ]
512
- }
513
- ]);
514
- expect(
515
- query.getLookups(
516
- normalizeTokenParameters({
517
- region_names: JSON.stringify(['colorado', 'texas'])
518
- })
519
- )
520
- ).toEqual([
521
- ['mybucket', '1', 'colorado'],
522
- ['mybucket', '1', 'texas']
523
- ]);
524
- });
525
-
526
- test('some math', () => {
527
- const rules = SqlSyncRules.fromYaml(`
528
- bucket_definitions:
529
- mybucket:
530
- data:
531
- - SELECT id, (5 / 2) AS int, (5 / 2.0) AS float, (CAST(5 AS real) / 2) AS float2 FROM assets
532
- `);
533
-
534
- expect(rules.evaluateRow({ sourceTable: ASSETS, record: { id: 'asset1' } })).toEqual([
535
- {
536
- ruleId: '1',
537
- bucket: 'mybucket[]',
538
- id: 'asset1',
539
- data: {
540
- id: 'asset1',
541
- int: 2n,
542
- float: 2.5,
543
- float2: 2.5
544
- },
545
- table: 'assets'
546
- }
547
- ]);
548
- });
549
-
550
- test('bucket with static numeric parameters', () => {
551
- const rules = SqlSyncRules.fromYaml(`
552
- bucket_definitions:
553
- mybucket:
554
- parameters: SELECT token_parameters.int1, token_parameters.float1, token_parameters.float2
555
- data:
556
- - SELECT id FROM assets WHERE assets.int1 = bucket.int1 AND assets.float1 = bucket.float1 AND assets.float2 = bucket.float2
557
- `);
558
- expect(rules.getStaticBucketIds(normalizeTokenParameters({ int1: 314, float1: 3.14, float2: 314 }))).toEqual([
559
- 'mybucket[314,3.14,314]'
560
- ]);
561
-
562
- expect(
563
- rules.evaluateRow({ sourceTable: ASSETS, record: { id: 'asset1', int1: 314n, float1: 3.14, float2: 314 } })
564
- ).toEqual([
565
- {
566
- ruleId: '1',
567
- bucket: 'mybucket[314,3.14,314]',
568
- id: 'asset1',
569
- data: {
570
- id: 'asset1'
571
- },
572
- table: 'assets'
573
- }
574
- ]);
575
- });
576
-
577
- test('bucket with queried numeric parameters', () => {
578
- const sql =
579
- 'SELECT users.int1, users.float1, users.float2 FROM users WHERE users.int1 = token_parameters.int1 AND users.float1 = token_parameters.float1 AND users.float2 = token_parameters.float2';
580
- const query = SqlParameterQuery.fromSql('mybucket', sql) as SqlParameterQuery;
581
- query.id = '1';
582
- // Note: We don't need to worry about numeric vs decimal types in the lookup - JSONB handles normalization for us.
583
- expect(query.evaluateParameterRow({ int1: 314n, float1: 3.14, float2: 314 })).toEqual([
584
- {
585
- lookup: ['mybucket', '1', 314n, 3.14, 314],
586
-
587
- bucket_parameters: [{ int1: 314n, float1: 3.14, float2: 314 }]
588
- }
589
- ]);
590
-
591
- // Similarly, we don't need to worry about the types here.
592
- // This test just checks the current behavior.
593
- expect(query.getLookups(normalizeTokenParameters({ int1: 314n, float1: 3.14, float2: 314 }))).toEqual([
594
- ['mybucket', '1', 314n, 3.14, 314n]
595
- ]);
596
-
597
- // We _do_ need to care about the bucket string representation.
598
- expect(query.resolveBucketIds([{ int1: 314, float1: 3.14, float2: 314 }], normalizeTokenParameters({}))).toEqual([
599
- 'mybucket[314,3.14,314]'
600
- ]);
601
-
602
- expect(query.resolveBucketIds([{ int1: 314n, float1: 3.14, float2: 314 }], normalizeTokenParameters({}))).toEqual([
603
- 'mybucket[314,3.14,314]'
604
- ]);
605
- });
606
-
607
- test('parameter query with token filter (1)', () => {
608
- // Also supported: token_parameters.is_admin = true
609
- // Not supported: token_parameters.is_admin != false
610
- // Support could be added later.
611
- const sql = 'SELECT FROM users WHERE users.id = token_parameters.user_id AND token_parameters.is_admin';
612
- const query = SqlParameterQuery.fromSql('mybucket', sql) as SqlParameterQuery;
613
- query.id = '1';
614
-
615
- expect(query.evaluateParameterRow({ id: 'user1' })).toEqual([
616
- {
617
- lookup: ['mybucket', '1', 'user1', 1n],
618
- bucket_parameters: [{}]
619
- }
620
- ]);
621
-
622
- expect(query.getLookups(normalizeTokenParameters({ user_id: 'user1', is_admin: true }))).toEqual([
623
- ['mybucket', '1', 'user1', 1n]
624
- ]);
625
- // Would not match any actual lookups
626
- expect(query.getLookups(normalizeTokenParameters({ user_id: 'user1', is_admin: false }))).toEqual([
627
- ['mybucket', '1', 'user1', 0n]
628
- ]);
629
- });
630
-
631
- test('parameter query with token filter (2)', () => {
632
- const sql =
633
- 'SELECT users.id AS user_id, token_parameters.is_admin as is_admin FROM users WHERE users.id = token_parameters.user_id AND token_parameters.is_admin';
634
- const query = SqlParameterQuery.fromSql('mybucket', sql) as SqlParameterQuery;
635
- query.id = '1';
636
-
637
- expect(query.evaluateParameterRow({ id: 'user1' })).toEqual([
638
- {
639
- lookup: ['mybucket', '1', 'user1', 1n],
640
-
641
- bucket_parameters: [{ user_id: 'user1' }]
642
- }
643
- ]);
644
-
645
- expect(query.getLookups(normalizeTokenParameters({ user_id: 'user1', is_admin: true }))).toEqual([
646
- ['mybucket', '1', 'user1', 1n]
647
- ]);
648
-
649
- expect(
650
- query.resolveBucketIds([{ user_id: 'user1' }], normalizeTokenParameters({ user_id: 'user1', is_admin: true }))
651
- ).toEqual(['mybucket["user1",1]']);
652
- });
653
-
654
- test('custom table and id', () => {
655
- const rules = SqlSyncRules.fromYaml(`
656
- bucket_definitions:
657
- mybucket:
658
- data:
659
- - SELECT client_id AS id, description FROM assets_123 as assets WHERE assets.archived = false
660
- - SELECT other_id AS id, description FROM assets_123 as assets
661
- `);
662
-
663
- expect(
664
- rules.evaluateRow({
665
- sourceTable: new TestSourceTable('assets_123'),
666
- record: { client_id: 'asset1', description: 'test', archived: 0n, other_id: 'other1' }
667
- })
668
- ).toEqual([
669
- {
670
- ruleId: '1',
671
- bucket: 'mybucket[]',
672
- id: 'asset1',
673
- data: {
674
- id: 'asset1',
675
- description: 'test'
676
- },
677
- table: 'assets'
678
- },
679
- {
680
- ruleId: '2',
681
- bucket: 'mybucket[]',
682
- id: 'other1',
683
- data: {
684
- id: 'other1',
685
- description: 'test'
686
- },
687
- table: 'assets'
688
- }
689
- ]);
690
- });
691
-
692
- test('wildcard table', () => {
693
- const rules = SqlSyncRules.fromYaml(`
694
- bucket_definitions:
695
- mybucket:
696
- data:
697
- - SELECT client_id AS id, description, _table_suffix as suffix, * FROM "assets_%" as assets WHERE assets.archived = false AND _table_suffix > '100'
698
- `);
699
-
700
- expect(
701
- rules.evaluateRow({
702
- sourceTable: new TestSourceTable('assets_123'),
703
- record: { client_id: 'asset1', description: 'test', archived: 0n, other_id: 'other1' }
704
- })
705
- ).toEqual([
706
- {
707
- ruleId: '1',
708
- bucket: 'mybucket[]',
709
- id: 'asset1',
710
- data: {
711
- id: 'asset1',
712
- description: 'test',
713
- suffix: '123',
714
- archived: 0n,
715
- client_id: 'asset1',
716
- other_id: 'other1'
717
- },
718
- table: 'assets'
719
- }
720
- ]);
721
- });
722
-
723
- test('wildcard without alias', () => {
724
- const rules = SqlSyncRules.fromYaml(`
725
- bucket_definitions:
726
- mybucket:
727
- data:
728
- - SELECT *, _table_suffix as suffix, * FROM "%" WHERE archived = false
729
- `);
730
-
731
- expect(
732
- rules.evaluateRow({
733
- sourceTable: ASSETS,
734
- record: { id: 'asset1', description: 'test', archived: 0n }
735
- })
736
- ).toEqual([
737
- {
738
- ruleId: '1',
739
- bucket: 'mybucket[]',
740
- id: 'asset1',
741
- data: {
742
- id: 'asset1',
743
- description: 'test',
744
- suffix: 'assets',
745
- archived: 0n
746
- },
747
- table: 'assets'
748
- }
749
- ]);
750
- });
751
-
752
- test('should filter schemas', () => {
753
- const rules = SqlSyncRules.fromYaml(`
754
- bucket_definitions:
755
- mybucket:
756
- data:
757
- - SELECT id FROM "assets" # Yes
758
- - SELECT id FROM "public"."assets" # yes
759
- - SELECT id FROM "default.public"."assets" # yes
760
- - SELECT id FROM "other"."assets" # no
761
- - SELECT id FROM "other.public"."assets" # no
762
- `);
763
-
764
- expect(
765
- rules.evaluateRow({
766
- sourceTable: ASSETS,
767
- record: { id: 'asset1' }
768
- })
769
- ).toEqual([
770
- {
771
- ruleId: '1',
772
- bucket: 'mybucket[]',
773
- id: 'asset1',
774
- data: {
775
- id: 'asset1'
776
- },
777
- table: 'assets'
778
- },
779
- {
780
- ruleId: '2',
781
- bucket: 'mybucket[]',
782
- id: 'asset1',
783
- data: {
784
- id: 'asset1'
785
- },
786
- table: 'assets'
787
- },
788
- {
789
- ruleId: '3',
790
- bucket: 'mybucket[]',
791
- id: 'asset1',
792
- data: {
793
- id: 'asset1'
794
- },
795
- table: 'assets'
796
- }
797
- ]);
798
- });
799
-
800
- test('case-sensitive parameter queries (1)', () => {
801
- const sql = 'SELECT users."userId" AS user_id FROM users WHERE users."userId" = token_parameters.user_id';
802
- const query = SqlParameterQuery.fromSql('mybucket', sql) as SqlParameterQuery;
803
- query.id = '1';
804
-
805
- expect(query.evaluateParameterRow({ userId: 'user1' })).toEqual([
806
- {
807
- lookup: ['mybucket', '1', 'user1'],
808
-
809
- bucket_parameters: [{ user_id: 'user1' }]
810
- }
811
- ]);
812
- });
813
-
814
- test('case-sensitive parameter queries (2)', () => {
815
- // Note: This documents current behavior.
816
- // This may change in the future - we should check against expected behavior for
817
- // Postgres and/or SQLite.
818
- const sql = 'SELECT users.userId AS user_id FROM users WHERE users.userId = token_parameters.user_id';
819
- const query = SqlParameterQuery.fromSql('mybucket', sql) as SqlParameterQuery;
820
- query.id = '1';
821
-
822
- expect(query.evaluateParameterRow({ userId: 'user1' })).toEqual([]);
823
- expect(query.evaluateParameterRow({ userid: 'user1' })).toEqual([
824
- {
825
- lookup: ['mybucket', '1', 'user1'],
826
-
827
- bucket_parameters: [{ user_id: 'user1' }]
828
- }
829
- ]);
830
- });
831
-
832
- test('dynamic global parameter query', () => {
833
- const sql = "SELECT workspaces.id AS workspace_id FROM workspaces WHERE visibility = 'public'";
834
- const query = SqlParameterQuery.fromSql('mybucket', sql) as SqlParameterQuery;
835
- query.id = '1';
836
-
837
- expect(query.evaluateParameterRow({ id: 'workspace1', visibility: 'public' })).toEqual([
838
- {
839
- lookup: ['mybucket', '1'],
840
-
841
- bucket_parameters: [{ workspace_id: 'workspace1' }]
842
- }
843
- ]);
844
-
845
- expect(query.evaluateParameterRow({ id: 'workspace1', visibility: 'private' })).toEqual([]);
846
- });
847
-
848
- test('invalid OR in parameter queries', () => {
849
- // Supporting this case is more tricky. We can do this by effectively denormalizing the OR clause
850
- // into separate queries, but it's a significant change. For now, developers should do that manually.
851
- const sql =
852
- "SELECT workspaces.id AS workspace_id FROM workspaces WHERE workspaces.user_id = token_parameters.user_id OR visibility = 'public'";
853
- const query = SqlParameterQuery.fromSql('mybucket', sql) as SqlParameterQuery;
854
- expect(query.errors[0].message).toMatch(/must use the same parameters/);
855
- });
856
-
857
- test('types', () => {
858
- const schema = new StaticSchema([
859
- {
860
- tag: SourceTable.DEFAULT_TAG,
861
- schemas: [
862
- {
863
- name: SourceTable.DEFAULT_SCHEMA,
864
- tables: [
865
- {
866
- name: 'assets',
867
- columns: [
868
- { name: 'id', pg_type: 'uuid' },
869
- { name: 'name', pg_type: 'text' },
870
- { name: 'count', pg_type: 'int4' },
871
- { name: 'owner_id', pg_type: 'uuid' }
872
- ]
873
- }
874
- ]
875
- }
876
- ]
877
- }
878
- ]);
879
-
880
- const q1 = SqlDataQuery.fromSql('q1', ['user_id'], `SELECT * FROM assets WHERE owner_id = bucket.user_id`);
881
- expect(q1.getColumnOutputs(schema)).toEqual([
882
- {
883
- name: 'assets',
884
- columns: [
885
- { name: 'id', type: ExpressionType.TEXT },
886
- { name: 'name', type: ExpressionType.TEXT },
887
- { name: 'count', type: ExpressionType.INTEGER },
888
- { name: 'owner_id', type: ExpressionType.TEXT }
889
- ]
890
- }
891
- ]);
892
-
893
- const q2 = SqlDataQuery.fromSql(
894
- 'q1',
895
- ['user_id'],
896
- `
897
- SELECT id :: integer as id,
898
- upper(name) as name_upper,
899
- hex('test') as hex,
900
- count + 2 as count2,
901
- count * 3.0 as count3,
902
- count * '4' as count4,
903
- name ->> '$.attr' as json_value,
904
- ifnull(name, 2.0) as maybe_name
905
- FROM assets WHERE owner_id = bucket.user_id`
906
- );
907
- expect(q2.getColumnOutputs(schema)).toEqual([
908
- {
909
- name: 'assets',
910
- columns: [
911
- { name: 'id', type: ExpressionType.INTEGER },
912
- { name: 'name_upper', type: ExpressionType.TEXT },
913
- { name: 'hex', type: ExpressionType.TEXT },
914
- { name: 'count2', type: ExpressionType.INTEGER },
915
- { name: 'count3', type: ExpressionType.REAL },
916
- { name: 'count4', type: ExpressionType.NUMERIC },
917
- { name: 'json_value', type: ExpressionType.ANY_JSON },
918
- { name: 'maybe_name', type: ExpressionType.TEXT.or(ExpressionType.REAL) }
919
- ]
920
- }
921
- ]);
922
- });
923
-
924
- test('validate columns', () => {
925
- const schema = new StaticSchema([
926
- {
927
- tag: SourceTable.DEFAULT_TAG,
928
- schemas: [
929
- {
930
- name: SourceTable.DEFAULT_SCHEMA,
931
- tables: [
932
- {
933
- name: 'assets',
934
- columns: [
935
- { name: 'id', pg_type: 'uuid' },
936
- { name: 'name', pg_type: 'text' },
937
- { name: 'count', pg_type: 'int4' },
938
- { name: 'owner_id', pg_type: 'uuid' }
939
- ]
940
- }
941
- ]
942
- }
943
- ]
944
- }
945
- ]);
946
- const q1 = SqlDataQuery.fromSql(
947
- 'q1',
948
- ['user_id'],
949
- 'SELECT id, name, count FROM assets WHERE owner_id = bucket.user_id',
950
- schema
951
- );
952
- expect(q1.errors).toEqual([]);
953
-
954
- const q2 = SqlDataQuery.fromSql(
955
- 'q2',
956
- ['user_id'],
957
- 'SELECT id, upper(description) as d FROM assets WHERE other_id = bucket.user_id',
958
- schema
959
- );
960
- expect(q2.errors).toMatchObject([
961
- {
962
- message: `Column not found: other_id`,
963
- type: 'warning'
964
- },
965
- {
966
- message: `Column not found: description`,
967
- type: 'warning'
968
- }
969
- ]);
970
-
971
- const q3 = SqlDataQuery.fromSql(
972
- 'q3',
973
- ['user_id'],
974
- 'SELECT id, description, * FROM nope WHERE other_id = bucket.user_id',
975
- schema
976
- );
977
- expect(q3.errors).toMatchObject([
978
- {
979
- message: `Table public.nope not found`,
980
- type: 'warning'
981
- }
982
- ]);
983
- });
984
-
985
- test('schema generation', () => {
986
- const schema = new StaticSchema([
987
- {
988
- tag: SourceTable.DEFAULT_TAG,
989
- schemas: [
990
- {
991
- name: SourceTable.DEFAULT_SCHEMA,
992
- tables: [
993
- {
994
- name: 'assets',
995
- columns: [
996
- { name: 'id', pg_type: 'uuid' },
997
- { name: 'name', pg_type: 'text' },
998
- { name: 'count', pg_type: 'int4' },
999
- { name: 'owner_id', pg_type: 'uuid' }
1000
- ]
1001
- }
1002
- ]
1003
- }
1004
- ]
1005
- }
1006
- ]);
1007
-
1008
- const rules = SqlSyncRules.fromYaml(`
1009
- bucket_definitions:
1010
- mybucket:
1011
- data:
1012
- - SELECT * FROM assets as assets1
1013
- - SELECT id, name, count FROM assets as assets2
1014
- - SELECT id, owner_id as other_id, foo FROM assets as ASSETS2
1015
- `);
1016
-
1017
- expect(new DartSchemaGenerator().generate(rules, schema)).toEqual(`Schema([
1018
- Table('assets1', [
1019
- Column.text('name'),
1020
- Column.integer('count'),
1021
- Column.text('owner_id')
1022
- ]),
1023
- Table('assets2', [
1024
- Column.text('name'),
1025
- Column.integer('count'),
1026
- Column.text('other_id'),
1027
- Column.text('foo')
1028
- ])
1029
- ]);
1030
- `);
1031
-
1032
- expect(new JsSchemaGenerator().generate(rules, schema)).toEqual(`new Schema([
1033
- new Table({
1034
- name: 'assets1',
1035
- columns: [
1036
- new Column({ name: 'name', type: ColumnType.TEXT }),
1037
- new Column({ name: 'count', type: ColumnType.INTEGER }),
1038
- new Column({ name: 'owner_id', type: ColumnType.TEXT })
1039
- ]
1040
- }),
1041
- new Table({
1042
- name: 'assets2',
1043
- columns: [
1044
- new Column({ name: 'name', type: ColumnType.TEXT }),
1045
- new Column({ name: 'count', type: ColumnType.INTEGER }),
1046
- new Column({ name: 'other_id', type: ColumnType.TEXT }),
1047
- new Column({ name: 'foo', type: ColumnType.TEXT })
1048
- ]
1049
- })
1050
- ])
1051
- `);
1052
- });
1053
- });