@saltcorn/admin-models 1.6.0 → 1.7.0-alpha.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.
Files changed (52) hide show
  1. package/cjs/index.cjs +18 -0
  2. package/cjs/models/backup.cjs +18 -0
  3. package/cjs/models/config-check.cjs +18 -0
  4. package/cjs/models/pack.cjs +18 -0
  5. package/cjs/models/snapshot.cjs +18 -0
  6. package/cjs/models/tenant.cjs +18 -0
  7. package/dist/index.d.ts +6 -5
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +12 -18
  10. package/dist/index.js.map +1 -1
  11. package/dist/models/backup.d.ts +3 -2
  12. package/dist/models/backup.d.ts.map +1 -1
  13. package/dist/models/backup.js +175 -167
  14. package/dist/models/backup.js.map +1 -1
  15. package/dist/models/config-check.d.ts +2 -1
  16. package/dist/models/config-check.d.ts.map +1 -1
  17. package/dist/models/config-check.js +16 -23
  18. package/dist/models/config-check.js.map +1 -1
  19. package/dist/models/pack.d.ts +2 -1
  20. package/dist/models/pack.d.ts.map +1 -1
  21. package/dist/models/pack.js +121 -150
  22. package/dist/models/pack.js.map +1 -1
  23. package/dist/models/snapshot.d.ts +1 -1
  24. package/dist/models/snapshot.d.ts.map +1 -1
  25. package/dist/models/snapshot.js +26 -29
  26. package/dist/models/snapshot.js.map +1 -1
  27. package/dist/models/tenant.d.ts +1 -1
  28. package/dist/models/tenant.d.ts.map +1 -1
  29. package/dist/models/tenant.js +43 -48
  30. package/dist/models/tenant.js.map +1 -1
  31. package/dist/tests/backup.test.d.ts +2 -0
  32. package/dist/tests/backup.test.d.ts.map +1 -0
  33. package/dist/tests/backup.test.js +201 -0
  34. package/dist/tests/backup.test.js.map +1 -0
  35. package/dist/tests/cfgcheck.test.d.ts +2 -0
  36. package/dist/tests/cfgcheck.test.d.ts.map +1 -0
  37. package/dist/tests/cfgcheck.test.js +32 -0
  38. package/dist/tests/cfgcheck.test.js.map +1 -0
  39. package/dist/tests/pack.test.d.ts +2 -0
  40. package/dist/tests/pack.test.d.ts.map +1 -0
  41. package/dist/tests/pack.test.js +769 -0
  42. package/dist/tests/pack.test.js.map +1 -0
  43. package/dist/tests/random.test.d.ts +2 -0
  44. package/dist/tests/random.test.d.ts.map +1 -0
  45. package/dist/tests/random.test.js +150 -0
  46. package/dist/tests/random.test.js.map +1 -0
  47. package/dist/tests/tenant.test.d.ts +2 -0
  48. package/dist/tests/tenant.test.d.ts.map +1 -0
  49. package/dist/tests/tenant.test.js +106 -0
  50. package/dist/tests/tenant.test.js.map +1 -0
  51. package/gen-cjs-shims.cjs +71 -0
  52. package/package.json +31 -27
@@ -0,0 +1,769 @@
1
+ import db from "@saltcorn/data/db/index";
2
+ import pack from "../models/pack.js";
3
+ const { table_pack, view_pack, plugin_pack, page_pack, page_group_pack, library_pack, trigger_pack, role_pack, tag_pack, model_pack, model_instance_pack, fetch_available_packs, fetch_pack_by_name, install_pack, can_install_pack, uninstall_pack, create_pack_from_tag, } = pack;
4
+ import * as utils from "@saltcorn/data/utils";
5
+ const { isStale } = utils;
6
+ import { getState } from "@saltcorn/data/db/state";
7
+ import basePlugin from "@saltcorn/data/base-plugin";
8
+ import reset from "@saltcorn/data/db/reset_schema";
9
+ import fixtures from "@saltcorn/data/db/fixtures";
10
+ import Table from "@saltcorn/data/models/table";
11
+ import Tag from "@saltcorn/data/models/tag";
12
+ import { afterAll, describe, it, expect, beforeAll, jest, } from "@saltcorn/db-common/test_expect";
13
+ import Trigger from "@saltcorn/data/models/trigger";
14
+ import PageGroup from "@saltcorn/data/models/page_group";
15
+ import WorkflowStep from "@saltcorn/data/models/workflow_step";
16
+ import { assertIsSet } from "@saltcorn/data/tests/assertions";
17
+ import View from "@saltcorn/data/models/view";
18
+ import { instanceOfType } from "@saltcorn/types/common_types";
19
+ //import Trigger from "@saltcorn/data/models/trigger";
20
+ getState().registerPlugin("base", basePlugin);
21
+ beforeAll(async () => {
22
+ await reset();
23
+ await fixtures();
24
+ });
25
+ afterAll(async () => {
26
+ await reset();
27
+ await fixtures();
28
+ await db.close();
29
+ });
30
+ jest.setTimeout(30000);
31
+ describe("pack create", () => {
32
+ // table packs
33
+ it("creates table pack", async () => {
34
+ const tpack = await table_pack("patients");
35
+ expect(tpack.fields.length > 1).toBe(true);
36
+ expect(tpack.name).toBe("patients");
37
+ });
38
+ it("creates table pack for patients 2", async () => {
39
+ const table = Table.findOne({ name: "patients" });
40
+ expect(table !== null).toBe(true);
41
+ const tpack = await table_pack(table !== null ? table : "patients");
42
+ expect(tpack.fields.length > 1).toBe(true);
43
+ expect(tpack.name).toBe("patients");
44
+ });
45
+ it("creates table pack for users", async () => {
46
+ const tpack = await table_pack("users");
47
+ expect(tpack.fields.length > 1).toBe(true);
48
+ expect(tpack.name).toBe("users");
49
+ });
50
+ it("creates table pack for non existing table", async () => {
51
+ try {
52
+ await table_pack("nonexist_table");
53
+ }
54
+ catch (error) {
55
+ expect(error.message).toMatch("Unable to find table 'nonexist_table'");
56
+ }
57
+ });
58
+ // view packs
59
+ it("creates view pack", async () => {
60
+ const vpack = await view_pack("authorlist");
61
+ expect(vpack).toEqual({
62
+ attributes: null,
63
+ configuration: {
64
+ columns: [
65
+ { field_name: "author", state_field: "on", type: "Field" },
66
+ { type: "ViewLink", view: "Own:authorshow" },
67
+ { action_name: "Delete", type: "Action" },
68
+ {
69
+ agg_field: "name",
70
+ agg_relation: "patients.favbook",
71
+ stat: "Count",
72
+ type: "Aggregation",
73
+ },
74
+ ],
75
+ },
76
+ exttable_name: null,
77
+ min_role: 100,
78
+ name: "authorlist",
79
+ description: "",
80
+ menu_label: undefined,
81
+ slug: null,
82
+ table: "books",
83
+ viewtemplate: "List",
84
+ default_render_page: null,
85
+ });
86
+ });
87
+ it("creates view pack for non existing view", async () => {
88
+ try {
89
+ await view_pack("nonexist_view");
90
+ }
91
+ catch (error) {
92
+ expect(error.message).toMatch("Unable to find view 'nonexist_view'");
93
+ }
94
+ });
95
+ // plugin packs
96
+ it("creates plugin pack", async () => {
97
+ const ppack = await plugin_pack("base");
98
+ expect(ppack).toStrictEqual({
99
+ configuration: null,
100
+ location: "@saltcorn/base-plugin",
101
+ name: "base",
102
+ deploy_private_key: null,
103
+ source: "npm",
104
+ version: "latest",
105
+ });
106
+ });
107
+ it("creates plugin pack for non existing plugin", async () => {
108
+ try {
109
+ await plugin_pack("nonexist_plugin");
110
+ }
111
+ catch (error) {
112
+ expect(error.message).toMatch("Unable to find plugin 'nonexist_plugin'");
113
+ }
114
+ });
115
+ // page packs
116
+ it("creates page pack", async () => {
117
+ const ppack = await page_pack("a_page");
118
+ expect(ppack).toEqual({
119
+ name: "a_page",
120
+ title: "grgw",
121
+ attributes: null,
122
+ description: "rgerg",
123
+ menu_label: undefined,
124
+ min_role: 100,
125
+ layout: {
126
+ above: [
127
+ {
128
+ type: "blank",
129
+ block: false,
130
+ contents: "Hello world",
131
+ textStyle: "",
132
+ },
133
+ { type: "line_break" },
134
+ { type: "blank", isHTML: true, contents: "<h1> foo</h1>" },
135
+ {
136
+ url: "https://saltcorn.com/",
137
+ text: "Click here",
138
+ type: "link",
139
+ block: false,
140
+ textStyle: "",
141
+ },
142
+ {
143
+ type: "card",
144
+ title: "header",
145
+ contents: {
146
+ above: [
147
+ null,
148
+ {
149
+ aligns: ["left", "left"],
150
+ widths: [6, 6],
151
+ besides: [
152
+ {
153
+ above: [
154
+ null,
155
+ {
156
+ type: "blank",
157
+ block: false,
158
+ contents: "Hello world",
159
+ textStyle: "",
160
+ },
161
+ ],
162
+ },
163
+ {
164
+ above: [
165
+ null,
166
+ {
167
+ type: "blank",
168
+ block: false,
169
+ contents: "Bye bye",
170
+ textStyle: "",
171
+ },
172
+ ],
173
+ },
174
+ ],
175
+ },
176
+ ],
177
+ },
178
+ },
179
+ ],
180
+ },
181
+ fixed_states: {},
182
+ root_page_for_roles: [],
183
+ });
184
+ });
185
+ it("creates page pack for non existing page", async () => {
186
+ try {
187
+ await page_pack("nonexist_page");
188
+ }
189
+ catch (error) {
190
+ expect(error.message).toMatch("Unable to find page 'nonexist_page'");
191
+ }
192
+ });
193
+ it("creates page group pack", async () => {
194
+ const groupPack = await page_group_pack("page_group");
195
+ expect(groupPack).toEqual({
196
+ name: "page_group",
197
+ description: null,
198
+ min_role: 100,
199
+ random_allocation: false,
200
+ members: [
201
+ {
202
+ page_name: "iPhone SE",
203
+ description: null,
204
+ sequence: 1,
205
+ eligible_formula: "width < 380 && height < 670 && user.id === 1 && locale === 'en'",
206
+ },
207
+ {
208
+ page_name: "iPhone XR",
209
+ description: null,
210
+ sequence: 2,
211
+ eligible_formula: "width < 415 && height < 900 && user.id === 1 && locale === 'en'",
212
+ },
213
+ {
214
+ page_name: "Surface Pro 7",
215
+ description: null,
216
+ sequence: 3,
217
+ eligible_formula: "width < 915 && height < 1370 && user.id === 1 && locale === 'en'",
218
+ },
219
+ {
220
+ page_name: "Laptop",
221
+ description: null,
222
+ sequence: 4,
223
+ eligible_formula: "width <= 1920 && height <= 1000 && user.id === 1 && locale === 'en'",
224
+ },
225
+ ],
226
+ });
227
+ });
228
+ it("creates page pack for non existing page group", async () => {
229
+ try {
230
+ await page_group_pack("nonexist_page_group");
231
+ }
232
+ catch (error) {
233
+ expect(error.message).toMatch("Unable to find page group 'nonexist_page_group'");
234
+ }
235
+ });
236
+ // todo library packs - needs to add library to fixture
237
+ // trigger packs
238
+ it("creates trigger pack", async () => {
239
+ // triggers
240
+ await Trigger.create({
241
+ name: "NeverActionTrigger",
242
+ action: "webhook",
243
+ description: "This is test trigger1",
244
+ //table_id: null
245
+ when_trigger: "Never",
246
+ configuration: {
247
+ // from https://requestbin.com/
248
+ // to inspect https://pipedream.com/sources/dc_jku44wk
249
+ url: "https://b6af540a71dce96ec130de5a0c47ada6.m.pipedream.net",
250
+ },
251
+ });
252
+ const trpack = await trigger_pack("NeverActionTrigger");
253
+ //expect(trpack.name ).toBe(true);
254
+ expect(trpack.when_trigger).toBe("Never");
255
+ });
256
+ it("creates trigger pack", async () => {
257
+ const trigger = await Trigger.create({
258
+ action: "Workflow",
259
+ when_trigger: "Never",
260
+ name: "mywf",
261
+ });
262
+ await WorkflowStep.create({
263
+ trigger_id: trigger.id,
264
+ name: "first_step",
265
+ next_step: "second_step",
266
+ action_name: "run_js_code",
267
+ initial_step: true,
268
+ configuration: { code: `return {x:1}` },
269
+ });
270
+ await WorkflowStep.create({
271
+ trigger_id: trigger.id,
272
+ name: "second_step",
273
+ next_step: "third_step",
274
+ action_name: "run_js_code",
275
+ initial_step: false,
276
+ configuration: { code: `return {y:x+1}` },
277
+ });
278
+ const trpack = await trigger_pack("mywf");
279
+ //expect(trpack.name ).toBe(true);
280
+ expect(trpack.when_trigger).toBe("Never");
281
+ expect(trpack.steps?.map((s) => s.name)).toContain("second_step");
282
+ });
283
+ // role packs
284
+ it("creates roles pack", async () => {
285
+ const rpack = await role_pack("admin");
286
+ expect(rpack.id === 1).toBe(true);
287
+ expect(rpack.role).toBe("admin");
288
+ });
289
+ // tag packs
290
+ it("creates tags pack", async () => {
291
+ const tpack = await tag_pack("tag1");
292
+ expect(tpack).toEqual({
293
+ name: "tag1",
294
+ entries: [
295
+ { table_name: "books" },
296
+ { view_name: "authorlist" },
297
+ { view_name: "authorshow" },
298
+ { view_name: "authoredit" },
299
+ ],
300
+ });
301
+ await expect((async () => await tag_pack("tag0"))()).rejects.toThrow();
302
+ });
303
+ // model packs
304
+ it("creates model pack", async () => {
305
+ const mpack = await model_pack("regression_model", "books");
306
+ expect(mpack).toEqual({
307
+ name: "regression_model",
308
+ table_name: "books",
309
+ modelpattern: "regression",
310
+ configuration: { numcluster: 2 },
311
+ });
312
+ await expect((async () => await model_pack("invalid_model", "books"))()).rejects.toThrow();
313
+ await expect((async () => await model_pack("regression_model", "invalid_table"))()).rejects.toThrow();
314
+ });
315
+ // model instance pack
316
+ it("creates model instance pack", async () => {
317
+ const mipack = await model_instance_pack("regression_model_instance", "regression_model", "books");
318
+ expect(mipack).toEqual({
319
+ name: "regression_model_instance",
320
+ table_name: "books",
321
+ state: {},
322
+ hyperparameters: { numcluster: 2 },
323
+ parameters: {},
324
+ metric_values: {},
325
+ trained_on: new Date("2019-11-11T10:34:00.000Z"),
326
+ fit_object: Buffer.from("foo"),
327
+ is_default: false,
328
+ report: "report",
329
+ model_name: "regression_model",
330
+ });
331
+ await expect((async () => await model_instance_pack("invalid_model_instance", "regression_model", "books"))()).rejects.toThrow();
332
+ await expect((async () => await model_instance_pack("regression_model_instance", "invalid_model", "books"))()).rejects.toThrow();
333
+ await expect((async () => await model_instance_pack("regression_model_instance", "regression_model", "invalid_table"))()).rejects.toThrow();
334
+ });
335
+ it("creates pack from tag", async () => {
336
+ const tag = await Tag.findOne({ name: "tag1" });
337
+ const pack = await create_pack_from_tag(tag);
338
+ expect(pack.tables.length).toBe(1);
339
+ expect(pack.tags.length).toBe(1);
340
+ expect(pack.tags[0].name).toBe("tag1");
341
+ });
342
+ });
343
+ // pack store
344
+ describe("pack store", () => {
345
+ let packs = new Array();
346
+ it("reset the pack store cache", async () => {
347
+ await getState().deleteConfig("available_packs");
348
+ await getState().deleteConfig("available_packs_fetched_at");
349
+ });
350
+ it("fetches the pack store", async () => {
351
+ packs = await fetch_available_packs();
352
+ expect(packs.length > 0).toBe(true);
353
+ const cache = getState().getConfig("available_packs", false);
354
+ expect(packs).toStrictEqual(cache);
355
+ });
356
+ it("fetches packs from the pack store", async () => {
357
+ const pack = await fetch_pack_by_name(packs[0].name);
358
+ // todo expect
359
+ // todo more cases
360
+ });
361
+ it("tries to fetch dummy pack from the pack store", async () => {
362
+ const nopack = await fetch_pack_by_name("nosuchpack");
363
+ expect(nopack).toBe(null);
364
+ });
365
+ it("caches the pack store", async () => {
366
+ const stored_at = getState().getConfig("available_packs_fetched_at", false);
367
+ expect(isStale(stored_at)).toBe(false);
368
+ await getState().setConfig("available_packs", []);
369
+ const packs1 = await fetch_available_packs();
370
+ expect(packs1).toEqual([]);
371
+ });
372
+ it("reset the pack store cache", async () => {
373
+ await getState().deleteConfig("available_packs");
374
+ await getState().deleteConfig("available_packs_fetched_at");
375
+ });
376
+ });
377
+ const todoPack = {
378
+ views: [
379
+ {
380
+ name: "EditTodo",
381
+ table: "TodoItems",
382
+ on_menu: false,
383
+ min_role: 100,
384
+ viewtemplate: "Edit",
385
+ configuration: {
386
+ fixed: { done: false },
387
+ layout: {
388
+ above: [
389
+ {
390
+ aligns: ["left", "left"],
391
+ widths: [2, 10],
392
+ besides: [
393
+ {
394
+ above: [
395
+ null,
396
+ {
397
+ type: "blank",
398
+ block: false,
399
+ contents: "Description",
400
+ textStyle: "",
401
+ },
402
+ ],
403
+ },
404
+ {
405
+ above: [
406
+ null,
407
+ {
408
+ type: "field",
409
+ block: false,
410
+ fieldview: "edit",
411
+ textStyle: "",
412
+ field_name: "description",
413
+ },
414
+ ],
415
+ },
416
+ ],
417
+ },
418
+ { type: "line_break" },
419
+ { type: "action", block: false, minRole: 100, action_name: "Save" },
420
+ ],
421
+ },
422
+ columns: [
423
+ { type: "Field", fieldview: "edit", field_name: "description" },
424
+ { type: "Action", minRole: 100, action_name: "Save" },
425
+ ],
426
+ viewname: "EditTodo",
427
+ view_when_done: "List Todos",
428
+ },
429
+ },
430
+ {
431
+ name: "List Todos",
432
+ table: "TodoItems",
433
+ min_role: 100,
434
+ menu_label: "List",
435
+ viewtemplate: "List",
436
+ configuration: {
437
+ columns: [
438
+ {
439
+ stat: "Count",
440
+ type: "Field",
441
+ view: "Own:EditTodo",
442
+ fieldview: "as_text",
443
+ field_name: "description",
444
+ join_field: "",
445
+ action_name: "Delete",
446
+ state_field: "on",
447
+ agg_relation: "",
448
+ },
449
+ {
450
+ stat: "Count",
451
+ type: "Field",
452
+ view: "Own:EditTodo",
453
+ fieldview: "show",
454
+ field_name: "done",
455
+ join_field: "",
456
+ action_name: "Delete",
457
+ state_field: "on",
458
+ agg_relation: "",
459
+ },
460
+ {
461
+ stat: "Count",
462
+ type: "Action",
463
+ view: "Own:EditTodo",
464
+ fieldview: "as_text",
465
+ field_name: "description",
466
+ join_field: "",
467
+ action_name: "Toggle done",
468
+ state_field: "on",
469
+ agg_relation: "",
470
+ },
471
+ ],
472
+ viewname: "List Todos",
473
+ default_state: { done: false, description: "" },
474
+ view_to_create: "EditTodo",
475
+ },
476
+ },
477
+ ],
478
+ tables: [
479
+ {
480
+ name: "TodoItems",
481
+ fields: [
482
+ {
483
+ name: "description",
484
+ type: "String",
485
+ label: "Description",
486
+ required: true,
487
+ is_unique: false,
488
+ attributes: { options: "" },
489
+ },
490
+ {
491
+ name: "done",
492
+ type: "Bool",
493
+ label: "Done",
494
+ required: true,
495
+ is_unique: false,
496
+ attributes: {},
497
+ },
498
+ ],
499
+ min_role_read: 1,
500
+ min_role_write: 1,
501
+ },
502
+ ],
503
+ pages: [
504
+ {
505
+ name: "FooPage",
506
+ menu_label: "FooPage",
507
+ title: "Foo",
508
+ description: "Foo",
509
+ layout: {},
510
+ min_role: 100,
511
+ root_page_for_roles: ["public"],
512
+ },
513
+ ],
514
+ page_groups: [
515
+ {
516
+ name: "FooPageGroup",
517
+ description: "Foo",
518
+ min_role: 100,
519
+ random_allocation: false,
520
+ members: [
521
+ {
522
+ page_name: "FooPage",
523
+ description: "Foo",
524
+ eligible_formula: "width < 380 && height < 670 && user.id === 1",
525
+ },
526
+ ],
527
+ },
528
+ ],
529
+ plugins: [],
530
+ roles: [],
531
+ library: [],
532
+ triggers: [],
533
+ tags: [],
534
+ models: [],
535
+ model_instances: [],
536
+ };
537
+ describe("pack install", () => {
538
+ it("installs pack", async () => {
539
+ const can = await can_install_pack(todoPack);
540
+ expect(can).toBe(true);
541
+ await install_pack(todoPack, "Todo list", () => { });
542
+ const tbl = Table.findOne({ name: "TodoItems" });
543
+ expect(!!tbl).toBe(true);
544
+ const menu = getState().getConfig("menu_items", []);
545
+ expect(menu).toContainEqual({
546
+ label: "List",
547
+ type: "View",
548
+ viewname: "List Todos",
549
+ min_role: 100,
550
+ });
551
+ expect(menu).toContainEqual({
552
+ label: "FooPage",
553
+ pagename: "FooPage",
554
+ type: "Page",
555
+ min_role: 100,
556
+ });
557
+ const pubhome = getState().getConfig("public_home", []);
558
+ expect(pubhome).toBe("FooPage");
559
+ const group = PageGroup.findOne({ name: "FooPageGroup" });
560
+ assertIsSet(group);
561
+ expect(group.members.length).toBe(1);
562
+ expect(group.members[0]).toEqual({
563
+ id: 5,
564
+ page_group_id: 2,
565
+ page_id: 8,
566
+ sequence: 1,
567
+ eligible_formula: "width < 380 && height < 670 && user.id === 1",
568
+ description: "Foo",
569
+ });
570
+ });
571
+ it("installs pack again", async () => {
572
+ await install_pack(todoPack, "Todo list", () => { });
573
+ });
574
+ it("uninstalls pack", async () => {
575
+ // todo make pack with trigger to cover all logic!
576
+ await uninstall_pack(todoPack, "Todo list");
577
+ const tbl = Table.findOne({ name: "TodoItems" });
578
+ expect(!!tbl).toBe(false);
579
+ });
580
+ it("installs pack updates view", async () => {
581
+ const newpack = JSON.parse(JSON.stringify(todoPack));
582
+ newpack.views[0].configuration.fixed.done = true;
583
+ const can = await can_install_pack(newpack);
584
+ expect(can).toBe(true);
585
+ await install_pack(newpack, "Todo list", () => { });
586
+ const view = View.findOne({ name: "EditTodo" });
587
+ expect(view?.configuration.fixed.done).toBe(true);
588
+ });
589
+ it("installs pack adds table field", async () => {
590
+ const newpack = JSON.parse(JSON.stringify(todoPack));
591
+ newpack.tables[0].fields.push({
592
+ name: "important",
593
+ label: "Important",
594
+ type: "Bool",
595
+ });
596
+ const can = await can_install_pack(newpack);
597
+ expect(can).toBe(true);
598
+ await install_pack(newpack, "Todo list", () => { });
599
+ const table = Table.findOne({ name: "TodoItems" });
600
+ assertIsSet(table);
601
+ const fimp = table.getField("important");
602
+ expect(instanceOfType(fimp?.type) && fimp?.type?.name).toBe("Bool");
603
+ });
604
+ it("uninstalls rls tables before rls pack tests", async () => {
605
+ for (const name of ["rls_field_owner_table", "rls_formula_owner_table"]) {
606
+ const t = Table.findOne({ name });
607
+ if (t)
608
+ await t.delete();
609
+ }
610
+ });
611
+ it("warns on clashing types", async () => {
612
+ const newpack = JSON.parse(JSON.stringify(todoPack));
613
+ newpack.tables[0].fields.push({
614
+ name: "important",
615
+ label: "Important",
616
+ type: "String",
617
+ });
618
+ const can = await can_install_pack(newpack);
619
+ expect(can).toStrictEqual({
620
+ warning: "Clashing field types for important on table TodoItems",
621
+ });
622
+ await install_pack(newpack, "Todo list", () => { });
623
+ const table = Table.findOne({ name: "TodoItems" });
624
+ assertIsSet(table);
625
+ const fimp = table.getField("important");
626
+ expect(instanceOfType(fimp?.type) && fimp?.type?.name).toBe("String");
627
+ });
628
+ });
629
+ // Two minimal pack fixtures used by the RLS tests below.
630
+ const rlsFieldOwnerPack = {
631
+ tables: [
632
+ {
633
+ name: "rls_field_owner_table",
634
+ fields: [
635
+ {
636
+ name: "title",
637
+ label: "Title",
638
+ type: "String",
639
+ required: false,
640
+ attributes: {},
641
+ },
642
+ {
643
+ name: "owner_id",
644
+ label: "Owner",
645
+ type: "Integer",
646
+ required: false,
647
+ attributes: {},
648
+ },
649
+ ],
650
+ min_role_read: 80,
651
+ min_role_write: 80,
652
+ rls_enabled: true,
653
+ ownership_field_name: "owner_id",
654
+ },
655
+ ],
656
+ views: [],
657
+ pages: [],
658
+ page_groups: [],
659
+ plugins: [],
660
+ roles: [],
661
+ library: [],
662
+ triggers: [],
663
+ tags: [],
664
+ models: [],
665
+ model_instances: [],
666
+ };
667
+ const rlsFormulaOwnerPack = {
668
+ tables: [
669
+ {
670
+ name: "rls_formula_owner_table",
671
+ fields: [
672
+ {
673
+ name: "title",
674
+ label: "Title",
675
+ type: "String",
676
+ required: false,
677
+ attributes: {},
678
+ },
679
+ {
680
+ name: "owner_id",
681
+ label: "Owner",
682
+ type: "Integer",
683
+ required: false,
684
+ attributes: {},
685
+ },
686
+ ],
687
+ min_role_read: 80,
688
+ min_role_write: 80,
689
+ rls_enabled: true,
690
+ ownership_formula: "user.id == row.owner_id",
691
+ },
692
+ ],
693
+ views: [],
694
+ pages: [],
695
+ page_groups: [],
696
+ plugins: [],
697
+ roles: [],
698
+ library: [],
699
+ triggers: [],
700
+ tags: [],
701
+ models: [],
702
+ model_instances: [],
703
+ };
704
+ describe("rls_enabled in packs", () => {
705
+ afterAll(async () => {
706
+ for (const name of ["rls_field_owner_table", "rls_formula_owner_table"]) {
707
+ const t = Table.findOne({ name });
708
+ if (t)
709
+ await t.delete();
710
+ }
711
+ });
712
+ it("table_pack serializes rls_enabled=false for a regular table", async () => {
713
+ const tpack = await table_pack("books");
714
+ expect(tpack.rls_enabled).toBeFalsy();
715
+ });
716
+ it("installs field-based RLS pack and sets rls_enabled=true with ownership_field_id", async () => {
717
+ await install_pack(rlsFieldOwnerPack, "RLS field owner test", () => { });
718
+ const table = Table.findOne({ name: "rls_field_owner_table" });
719
+ assertIsSet(table);
720
+ expect(table.rls_enabled).toBe(true);
721
+ expect(table.ownership_field_id).toBeTruthy();
722
+ });
723
+ it("table_pack round-trips rls_enabled=true with field-based ownership", async () => {
724
+ const tpack = await table_pack("rls_field_owner_table");
725
+ expect(tpack.rls_enabled).toBe(true);
726
+ expect(tpack.ownership_field_name).toBe("owner_id");
727
+ });
728
+ it("field-based RLS pack install enables RLS in PostgreSQL", async () => {
729
+ if (db.isSQLite)
730
+ return;
731
+ const schema = db.getTenantSchema();
732
+ const cls = await db.query(`SELECT relrowsecurity, relforcerowsecurity
733
+ FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
734
+ WHERE c.relname = $1 AND n.nspname = $2`, ["rls_field_owner_table", schema]);
735
+ expect(cls.rows[0]?.relrowsecurity).toBe(true);
736
+ expect(cls.rows[0]?.relforcerowsecurity).toBe(true);
737
+ const polRes = await db.query(`SELECT policyname FROM pg_catalog.pg_policies
738
+ WHERE schemaname = $1 AND tablename = $2`, [schema, "rls_field_owner_table"]);
739
+ expect(polRes.rows.length).toBeGreaterThan(0);
740
+ });
741
+ it("installs formula-only RLS pack and sets rls_enabled=true with ownership_formula", async () => {
742
+ await install_pack(rlsFormulaOwnerPack, "RLS formula owner test", () => { });
743
+ const table = Table.findOne({ name: "rls_formula_owner_table" });
744
+ assertIsSet(table);
745
+ expect(table.rls_enabled).toBe(true);
746
+ expect(table.ownership_formula).toBe("user.id == row.owner_id");
747
+ expect(table.ownership_field_id).toBeFalsy();
748
+ });
749
+ it("table_pack round-trips rls_enabled=true with formula-only ownership", async () => {
750
+ const tpack = await table_pack("rls_formula_owner_table");
751
+ expect(tpack.rls_enabled).toBe(true);
752
+ expect(tpack.ownership_formula).toBe("user.id == row.owner_id");
753
+ expect(tpack.ownership_field_name).toBeFalsy();
754
+ });
755
+ it("formula-only RLS pack install enables RLS in PostgreSQL", async () => {
756
+ if (db.isSQLite)
757
+ return;
758
+ const schema = db.getTenantSchema();
759
+ const cls = await db.query(`SELECT relrowsecurity, relforcerowsecurity
760
+ FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
761
+ WHERE c.relname = $1 AND n.nspname = $2`, ["rls_formula_owner_table", schema]);
762
+ expect(cls.rows[0]?.relrowsecurity).toBe(true);
763
+ expect(cls.rows[0]?.relforcerowsecurity).toBe(true);
764
+ const polRes = await db.query(`SELECT policyname FROM pg_catalog.pg_policies
765
+ WHERE schemaname = $1 AND tablename = $2`, [schema, "rls_formula_owner_table"]);
766
+ expect(polRes.rows.length).toBeGreaterThan(0);
767
+ });
768
+ });
769
+ //# sourceMappingURL=pack.test.js.map