json-rest-api 1.0.21 → 1.0.23

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 (88) hide show
  1. package/CLAUDE.md +1 -2
  2. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +180 -0
  3. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +172 -0
  4. package/docs/API.md +51 -14
  5. package/docs/COMPARISON.md +2 -2
  6. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +28 -1
  7. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +5 -1
  8. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +3 -1
  9. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +12 -60
  10. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +2 -2
  11. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -1
  12. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +2 -0
  13. package/docs/GUIDE/GUIDE_X_Autofiltering.md +355 -0
  14. package/docs/GUIDE/GUIDE_X_Fastify.md +78 -0
  15. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +250 -0
  16. package/docs/GUIDE/GUIDE_X_Query_Projections.md +159 -0
  17. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +199 -0
  18. package/docs/GUIDE/index.md +18 -9
  19. package/docs/QUICKSTART.md +5 -1
  20. package/docs/index.md +1 -1
  21. package/index.js +3 -2
  22. package/package.json +4 -4
  23. package/plugins/core/connectors/express-plugin.js +97 -184
  24. package/plugins/core/connectors/fastify-plugin.js +206 -0
  25. package/plugins/core/connectors/lib/connector-core.js +195 -0
  26. package/plugins/core/connectors/lib/transport-http-helpers.js +122 -0
  27. package/plugins/core/connectors/lib/transport-route-schemas.js +92 -0
  28. package/plugins/core/lib/dbIntrospection.js +1078 -0
  29. package/plugins/core/lib/dbTablesOperations.js +1104 -160
  30. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +5 -2
  31. package/plugins/core/lib/querying/knex-process-includes.js +3 -4
  32. package/plugins/core/lib/querying/knex-query-helpers.js +17 -71
  33. package/plugins/core/lib/querying/knex-relationship-includes.js +466 -206
  34. package/plugins/core/lib/querying/query-builder-utils.js +19 -0
  35. package/plugins/core/lib/querying/query-field-sort-helpers.js +135 -0
  36. package/plugins/core/lib/querying/sort-helpers.js +33 -0
  37. package/plugins/core/lib/querying/storage-adapter-utils.js +51 -0
  38. package/plugins/core/lib/querying/url-helpers.js +8 -2
  39. package/plugins/core/lib/querying-writing/compile-schemas.js +12 -1
  40. package/plugins/core/lib/querying-writing/knex-field-helpers.js +83 -13
  41. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +13 -12
  42. package/plugins/core/lib/querying-writing/query-field-helpers.js +88 -0
  43. package/plugins/core/lib/querying-writing/request-contracts.js +590 -0
  44. package/plugins/core/lib/storage/canonical-storage-mapping.js +113 -0
  45. package/plugins/core/lib/storage/storage-adapter.js +79 -2
  46. package/plugins/core/lib/storage/storage-mapping.js +204 -0
  47. package/plugins/core/rest-api-anyapi-knex-plugin.js +355 -228
  48. package/plugins/core/rest-api-autofilter-plugin.js +414 -0
  49. package/plugins/core/rest-api-knex-plugin.js +249 -211
  50. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +10 -5
  51. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +12 -6
  52. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +41 -0
  53. package/plugins/core/rest-api-plugin-methods/common.js +121 -83
  54. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +6 -1
  55. package/plugins/core/rest-api-plugin-methods/get.js +23 -14
  56. package/plugins/core/rest-api-plugin-methods/patch.js +28 -12
  57. package/plugins/core/rest-api-plugin-methods/post.js +13 -8
  58. package/plugins/core/rest-api-plugin-methods/put.js +45 -21
  59. package/plugins/core/rest-api-plugin-methods/query.js +15 -53
  60. package/plugins/core/rest-api-plugin.js +2 -0
  61. package/plugins/core/rest-api-positioning-plugin.js +1 -1
  62. package/plugins/core/rest-api-query-projections-plugin.js +130 -0
  63. package/plugins/core/socketio-plugin.js +1 -3
  64. package/tests/autofilter.test.js +463 -0
  65. package/tests/custom-id-simplified-post.test.js +75 -0
  66. package/tests/custom-idproperty-relationships.test.js +33 -0
  67. package/tests/db-introspection.test.js +518 -0
  68. package/tests/db-table-operations.test.js +781 -0
  69. package/tests/fastify-plugin.test.js +141 -0
  70. package/tests/field-getters.test.js +1 -2
  71. package/tests/field-setters.test.js +0 -4
  72. package/tests/fixtures/api-configs.js +188 -219
  73. package/tests/helpers/fake-fastify.js +99 -0
  74. package/tests/http-connectors-parity.test.js +235 -0
  75. package/tests/projected-fields.test.js +319 -0
  76. package/tests/relationships.test.js +3 -3
  77. package/tests/searchschema-knex-behavior.test.js +187 -0
  78. package/tests/searchschema-merge.test.js +21 -69
  79. package/tests/storage-mapping.test.js +210 -0
  80. package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +0 -707
  81. package/docs/GUIDE/GUIDE_X_Multihome.md +0 -666
  82. package/plugins/core/lib/querying-writing/payload-validators.js +0 -1314
  83. package/plugins/core/rest-api-access.js +0 -459
  84. package/plugins/core/rest-api-multihome-plugin.js +0 -314
  85. package/tests/multihome.test.js +0 -553
  86. package/tests/multihome.test.js.bak +0 -550
  87. package/tests/ownership-optout.test.js +0 -76
  88. package/tests/rest-api-access.test.js +0 -206
@@ -1,550 +0,0 @@
1
- import { describe, it, before, after, beforeEach } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import knexLib from 'knex';
4
- import {
5
- validateJsonApiStructure,
6
- cleanTables,
7
- createJsonApiDocument,
8
- assertResourceAttributes,
9
- createRelationship,
10
- resourceIdentifier
11
- } from './helpers/test-utils.js';
12
- import { createMultiHomeApi } from './fixtures/api-configs.js';
13
-
14
- // Create Knex instance for tests - always use SQLite in-memory
15
- const knex = knexLib({
16
- client: 'sqlite3',
17
- connection: {
18
- filename: ':memory:'
19
- },
20
- useNullAsDefault: true
21
- });
22
-
23
- // API instance that persists across ALL tests
24
- let api;
25
-
26
- describe('MultiHome Plugin Tests', () => {
27
- // IMPORTANT: before() runs ONCE for the entire test suite
28
- before(async () => {
29
- // Create API instance ONCE - this is reused for all tests
30
- api = await createMultiHomeApi(knex);
31
- });
32
-
33
- // IMPORTANT: after() cleans up resources
34
- after(async () => {
35
- // Always destroy knex connection to allow tests to exit
36
- await knex.destroy();
37
- });
38
-
39
- // IMPORTANT: beforeEach() cleans data but does NOT recreate API
40
- beforeEach(async () => {
41
- // Clean all tables - list ALL tables your tests use
42
- await cleanTables(knex, [
43
- 'multihome_projects',
44
- 'multihome_tasks',
45
- 'multihome_users',
46
- 'multihome_system_settings'
47
- ]);
48
- });
49
-
50
- describe('Tenant Isolation', () => {
51
- it('should automatically set tenant_id on new records', async () => {
52
- // Create a project for tenant A
53
- const doc = createJsonApiDocument('projects', {
54
- name: 'Tenant A Project',
55
- description: 'A project for tenant A'
56
- });
57
-
58
- // Create with tenant context
59
- const createResult = await api.resources.projects.post({
60
- inputRecord: doc,
61
- simplified: false
62
- }, {
63
- auth: { multihome_id: 'tenant-a' }
64
- });
65
-
66
- // Verify response
67
- validateJsonApiStructure(createResult);
68
- assert.equal(createResult.data.type, 'projects');
69
- assertResourceAttributes(createResult.data, {
70
- name: 'Tenant A Project',
71
- description: 'A project for tenant A',
72
- tenant_id: 'tenant-a' // Should be automatically set
73
- });
74
- });
75
-
76
- it('should filter queries by tenant_id', async () => {
77
- // Create projects for different tenants
78
- const projectA1 = await api.resources.projects.post({
79
- inputRecord: createJsonApiDocument('projects', {
80
- name: 'Project A1'
81
- }),
82
- simplified: false,
83
- auth: { multihome_id: 'tenant-a' }
84
- });
85
-
86
- const projectA2 = await api.resources.projects.post({
87
- inputRecord: createJsonApiDocument('projects', {
88
- name: 'Project A2'
89
- }),
90
- simplified: false,
91
- auth: { multihome_id: 'tenant-a' }
92
- });
93
-
94
- const projectB1 = await api.resources.projects.post({
95
- inputRecord: createJsonApiDocument('projects', {
96
- name: 'Project B1'
97
- }),
98
- simplified: false
99
- }, {
100
- auth: { multihome_id: 'tenant-b' }
101
- });
102
-
103
- // Query as tenant A - should only see tenant A's projects
104
- const resultA = await api.resources.projects.query({
105
- simplified: false
106
- }, {
107
- auth: { multihome_id: 'tenant-a' }
108
- });
109
-
110
- validateJsonApiStructure(resultA);
111
- assert.equal(resultA.data.length, 2, 'Tenant A should see 2 projects');
112
- assert(resultA.data.every(p => p.attributes.name.startsWith('Project A')));
113
-
114
- // Query as tenant B - should only see tenant B's projects
115
- const resultB = await api.resources.projects.query({
116
- simplified: false
117
- }, {
118
- auth: { multihome_id: 'tenant-b' }
119
- });
120
-
121
- validateJsonApiStructure(resultB);
122
- assert.equal(resultB.data.length, 1, 'Tenant B should see 1 project');
123
- assert.equal(resultB.data[0].attributes.name, 'Project B1');
124
- });
125
-
126
- it('should prevent access to other tenant records', async () => {
127
- // Create a project for tenant A
128
- const projectA = await api.resources.projects.post({
129
- inputRecord: createJsonApiDocument('projects', {
130
- name: 'Tenant A Secret Project'
131
- }),
132
- simplified: false
133
- }, {
134
- auth: { multihome_id: 'tenant-a' }
135
- });
136
-
137
- // Try to access as tenant B - should fail
138
- await assert.rejects(
139
- async () => {
140
- await api.resources.projects.get({
141
- id: projectA.data.id,
142
- simplified: false
143
- }, {
144
- auth: { multihome_id: 'tenant-b' }
145
- });
146
- },
147
- (err) => {
148
- return err.code === 'REST_API_RESOURCE';
149
- },
150
- 'Should not be able to access other tenant data'
151
- );
152
- });
153
-
154
- it('should prevent updating tenant_id to different value', async () => {
155
- // Create a project for tenant A
156
- const project = await api.resources.projects.post({
157
- inputRecord: createJsonApiDocument('projects', {
158
- name: 'Original Project'
159
- }),
160
- simplified: false
161
- }, {
162
- auth: { multihome_id: 'tenant-a' }
163
- });
164
-
165
- // Try to update with different tenant_id - should fail
166
- await assert.rejects(
167
- async () => {
168
- await api.resources.projects.patch({
169
- inputRecord: createJsonApiDocument('projects', {
170
- name: 'Updated Project',
171
- tenant_id: 'tenant-b' // Trying to change tenant
172
- }, null, project.data.id),
173
- simplified: false
174
- }, {
175
- auth: { multihome_id: 'tenant-a' }
176
- });
177
- },
178
- (err) => {
179
- return err.message.includes('must match current context');
180
- },
181
- 'Should not be able to change tenant_id'
182
- );
183
- });
184
- });
185
-
186
- describe('Relationships Across Tenants', () => {
187
- it('should maintain tenant isolation for related records', async () => {
188
- // Create projects for different tenants
189
- const projectA = await api.resources.projects.post({
190
- inputRecord: createJsonApiDocument('projects', {
191
- name: 'Tenant A Project'
192
- }),
193
- simplified: false
194
- }, {
195
- auth: { multihome_id: 'tenant-a' }
196
- });
197
-
198
- const projectB = await api.resources.projects.post({
199
- inputRecord: createJsonApiDocument('projects', {
200
- name: 'Tenant B Project'
201
- }),
202
- simplified: false
203
- }, {
204
- auth: { multihome_id: 'tenant-b' }
205
- });
206
-
207
- // Create task for tenant A project
208
- const taskA = await api.resources.tasks.post({
209
- inputRecord: createJsonApiDocument('tasks',
210
- {
211
- title: 'Task for Tenant A'
212
- },
213
- {
214
- project: createRelationship(resourceIdentifier('projects', projectA.data.id))
215
- }
216
- ),
217
- simplified: false
218
- }, {
219
- auth: { multihome_id: 'tenant-a' }
220
- });
221
-
222
- // Try to create task for tenant B project as tenant A - should fail
223
- await assert.rejects(
224
- async () => {
225
- await api.resources.tasks.post({
226
- inputRecord: createJsonApiDocument('tasks',
227
- {
228
- title: 'Invalid Task'
229
- },
230
- {
231
- project: createRelationship(resourceIdentifier('projects', projectB.data.id))
232
- }
233
- ),
234
- simplified: false
235
- }, {
236
- auth: { multihome_id: 'tenant-a' }
237
- });
238
- },
239
- (err) => {
240
- // Should fail because project belongs to different tenant
241
- return err.code === 'REST_API_VALIDATION' || err.code === 'REST_API_RESOURCE';
242
- },
243
- 'Should not be able to link to other tenant resources'
244
- );
245
-
246
- // Query with includes - should only see own tenant data
247
- const projectWithTasks = await api.resources.projects.get({
248
- id: projectA.data.id,
249
- queryParams: {
250
- include: ['tasks']
251
- },
252
- simplified: false
253
- }, {
254
- auth: { multihome_id: 'tenant-a' }
255
- });
256
-
257
- validateJsonApiStructure(projectWithTasks);
258
- assert(projectWithTasks.included, 'Should have included tasks');
259
- assert.equal(projectWithTasks.included.length, 1, 'Should include one task');
260
- assert.equal(projectWithTasks.included[0].attributes.tenant_id, 'tenant-a');
261
- });
262
- });
263
-
264
- describe('Excluded Resources', () => {
265
- it('should allow access to excluded resources without tenant context', async () => {
266
- // Create system setting without tenant context
267
- const setting = await api.resources.system_settings.post({
268
- inputRecord: createJsonApiDocument('system_settings', {
269
- key: 'app.version',
270
- value: '1.0.0'
271
- }),
272
- simplified: false
273
- }, {
274
- // No auth context
275
- });
276
-
277
- validateJsonApiStructure(setting);
278
- assert.equal(setting.data.type, 'system_settings');
279
- assert(!setting.data.attributes.tenant_id, 'Should not have tenant_id');
280
-
281
- // Query without tenant context should work
282
- const allSettings = await api.resources.system_settings.query({
283
- simplified: false
284
- }, {
285
- // No auth context
286
- });
287
-
288
- assert.equal(allSettings.data.length, 1);
289
-
290
- // Should be accessible by any tenant
291
- const settingByTenantA = await api.resources.system_settings.get({
292
- id: setting.data.id,
293
- simplified: false
294
- }, {
295
- auth: { multihome_id: 'tenant-a' }
296
- });
297
-
298
- assert.equal(settingByTenantA.data.id, setting.data.id);
299
- });
300
- });
301
-
302
- describe('Configuration Options', () => {
303
- it('should require tenant context when requireAuth is true', async () => {
304
- // Try to create without tenant context - should fail
305
- await assert.rejects(
306
- async () => {
307
- await api.resources.projects.post({
308
- inputRecord: createJsonApiDocument('projects', {
309
- name: 'No Tenant Project'
310
- }),
311
- simplified: false
312
- }, {
313
- // No auth context
314
- });
315
- },
316
- (err) => {
317
- return err.message.includes('multihome context');
318
- },
319
- 'Should require tenant context when requireAuth is true'
320
- );
321
- });
322
-
323
- it('should validate resources have tenant field when allowMissing is false', async () => {
324
- // This test validates that the plugin threw an error during setup
325
- // if a resource was missing the tenant field. Since our test resources
326
- // all have tenant_id (except excluded ones), we can verify the
327
- // configuration is working by checking vars
328
- assert.equal(api.vars.multihome.field, 'tenant_id');
329
- assert.equal(api.vars.multihome.allowMissing, false);
330
- assert(api.vars.multihome.excludeResources.includes('system_settings'));
331
- });
332
- });
333
-
334
- describe('Complex Queries', () => {
335
- it('should apply tenant filter to filtered queries', async () => {
336
- // Create users for different tenants
337
- await api.resources.users.post({
338
- inputRecord: createJsonApiDocument('users', {
339
- email: 'admin@tenant-a.com',
340
- name: 'Admin A',
341
- role: 'admin'
342
- }),
343
- simplified: false
344
- }, {
345
- auth: { multihome_id: 'tenant-a' }
346
- });
347
-
348
- await api.resources.users.post({
349
- inputRecord: createJsonApiDocument('users', {
350
- email: 'user@tenant-a.com',
351
- name: 'User A',
352
- role: 'member'
353
- }),
354
- simplified: false
355
- }, {
356
- auth: { multihome_id: 'tenant-a' }
357
- });
358
-
359
- await api.resources.users.post({
360
- inputRecord: createJsonApiDocument('users', {
361
- email: 'admin@tenant-b.com',
362
- name: 'Admin B',
363
- role: 'admin'
364
- }),
365
- simplified: false
366
- }, {
367
- auth: { multihome_id: 'tenant-b' }
368
- });
369
-
370
- // Query for admins as tenant A - should only see tenant A admin
371
- const adminsA = await api.resources.users.query({
372
- queryParams: {
373
- filters: { role: 'admin' }
374
- },
375
- simplified: false
376
- }, {
377
- auth: { multihome_id: 'tenant-a' }
378
- });
379
-
380
- assert.equal(adminsA.data.length, 1, 'Should only see tenant A admin');
381
- assert.equal(adminsA.data[0].attributes.email, 'admin@tenant-a.com');
382
-
383
- // Query for all users as tenant B - should only see tenant B users
384
- const usersB = await api.resources.users.query({
385
- simplified: false
386
- }, {
387
- auth: { multihome_id: 'tenant-b' }
388
- });
389
-
390
- assert.equal(usersB.data.length, 1, 'Should only see tenant B users');
391
- assert.equal(usersB.data[0].attributes.email, 'admin@tenant-b.com');
392
- });
393
-
394
- it('should apply tenant filter with sorting and pagination', async () => {
395
- // Create multiple projects for tenant A
396
- for (let i = 1; i <= 5; i++) {
397
- await api.resources.projects.post({
398
- inputRecord: createJsonApiDocument('projects', {
399
- name: `Project A${i}`,
400
- status: i % 2 === 0 ? 'active' : 'inactive'
401
- }),
402
- simplified: false
403
- }, {
404
- auth: { multihome_id: 'tenant-a' }
405
- });
406
- }
407
-
408
- // Create projects for tenant B
409
- for (let i = 1; i <= 3; i++) {
410
- await api.resources.projects.post({
411
- inputRecord: createJsonApiDocument('projects', {
412
- name: `Project B${i}`
413
- }),
414
- simplified: false
415
- }, {
416
- auth: { multihome_id: 'tenant-b' }
417
- });
418
- }
419
-
420
- // Query with pagination as tenant A
421
- const page1 = await api.resources.projects.query({
422
- queryParams: {
423
- page: { size: 2, number: 1 },
424
- sort: ['-name'] // Sort by name descending
425
- },
426
- simplified: false
427
- }, {
428
- auth: { multihome_id: 'tenant-a' }
429
- });
430
-
431
- assert.equal(page1.data.length, 2, 'Should return page size');
432
- assert.equal(page1.data[0].attributes.name, 'Project A5', 'Should be sorted descending');
433
- assert.equal(page1.data[1].attributes.name, 'Project A4');
434
-
435
- // Get page 2
436
- const page2 = await api.resources.projects.query({
437
- queryParams: {
438
- page: { size: 2, number: 2 },
439
- sort: ['-name']
440
- },
441
- simplified: false
442
- }, {
443
- auth: { multihome_id: 'tenant-a' }
444
- });
445
-
446
- assert.equal(page2.data.length, 2, 'Should return page size');
447
- assert(page2.data.every(p => p.attributes.tenant_id === 'tenant-a'));
448
- });
449
- });
450
-
451
- describe('Error Handling', () => {
452
- it('should provide clear error when no tenant context available', async () => {
453
- await assert.rejects(
454
- async () => {
455
- await api.resources.projects.query({
456
- simplified: false
457
- }, {
458
- // No auth context
459
- });
460
- },
461
- (err) => {
462
- return err.message.includes('No multihome context available');
463
- },
464
- 'Should provide clear error message'
465
- );
466
- });
467
-
468
- it('should handle missing tenant field gracefully for excluded resources', async () => {
469
- // system_settings is excluded and doesn't have tenant_id field
470
- const result = await api.resources.system_settings.post({
471
- inputRecord: createJsonApiDocument('system_settings', {
472
- key: 'test.setting',
473
- value: 'test'
474
- }),
475
- simplified: false
476
- }, {
477
- auth: { multihome_id: 'tenant-a' } // Should be ignored for excluded resources
478
- });
479
-
480
- validateJsonApiStructure(result);
481
- assert(!result.data.attributes.tenant_id, 'Excluded resource should not have tenant_id');
482
- });
483
- });
484
-
485
- describe('Security', () => {
486
- it('should prevent bypassing tenant filter with malicious IDs', async () => {
487
- // Create a project for tenant A
488
- const project = await api.resources.projects.post({
489
- inputRecord: createJsonApiDocument('projects', {
490
- name: 'Secure Project'
491
- }),
492
- simplified: false
493
- }, {
494
- auth: { multihome_id: 'tenant-a' }
495
- });
496
-
497
- // Try various bypass attempts as tenant B
498
- const maliciousIds = [
499
- project.data.id,
500
- `${project.data.id} OR 1=1`,
501
- `${project.data.id}'; DROP TABLE projects; --`
502
- ];
503
-
504
- for (const id of maliciousIds) {
505
- await assert.rejects(
506
- async () => {
507
- await api.resources.projects.get({
508
- id,
509
- simplified: false
510
- }, {
511
- auth: { multihome_id: 'tenant-b' }
512
- });
513
- },
514
- undefined, // Any error is fine
515
- `Should not bypass security with ID: ${id}`
516
- );
517
- }
518
- });
519
-
520
- it('should log security violations', async () => {
521
- // This test verifies that the plugin logs security violations
522
- // In a real implementation, you might capture logs and verify
523
- // For now, we just ensure the security check happens
524
-
525
- // Create data for tenant A
526
- const project = await api.resources.projects.post({
527
- inputRecord: createJsonApiDocument('projects', {
528
- name: 'Tenant A Data'
529
- }),
530
- simplified: false
531
- }, {
532
- auth: { multihome_id: 'tenant-a' }
533
- });
534
-
535
- // Attempt access as tenant B
536
- try {
537
- await api.resources.projects.get({
538
- id: project.data.id,
539
- simplified: false
540
- }, {
541
- auth: { multihome_id: 'tenant-b' }
542
- });
543
- assert.fail('Should have thrown error');
544
- } catch (err) {
545
- // Error is expected - security violation should be logged
546
- assert(err.code === 'REST_API_RESOURCE' || err.message.includes('Access denied'));
547
- }
548
- });
549
- });
550
- });
@@ -1,76 +0,0 @@
1
- import { describe, it, before, after, beforeEach } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import knexLib from 'knex'
4
- import {
5
- validateJsonApiStructure,
6
- cleanTables,
7
- createJsonApiDocument,
8
- assertResourceAttributes
9
- } from './helpers/test-utils.js'
10
- import { createOwnershipOptOutApi } from './fixtures/api-configs.js'
11
-
12
- const knex = knexLib({
13
- client: 'better-sqlite3',
14
- connection: { filename: ':memory:' },
15
- useNullAsDefault: true
16
- })
17
-
18
- let api
19
-
20
- async function createPublicItem (resourceName, name, authUserId) {
21
- const doc = createJsonApiDocument(resourceName, { name })
22
- const res = await api.resources[resourceName].post({
23
- inputRecord: doc,
24
- simplified: false
25
- }, { auth: { userId: authUserId } })
26
-
27
- validateJsonApiStructure(res)
28
- assertResourceAttributes(res.data, { name })
29
- return res.data
30
- }
31
-
32
- describe('Ownership opt-out (ownerField:false / ownership:false)', () => {
33
- before(async () => {
34
- api = await createOwnershipOptOutApi(knex)
35
- })
36
-
37
- after(async () => {
38
- await knex.destroy()
39
- })
40
-
41
- beforeEach(async () => {
42
- await cleanTables(knex, [
43
- 'ownopt_public_items_of',
44
- 'ownopt_public_items_own'
45
- ])
46
- })
47
-
48
- it('ownerField:false - allows create+query without user_id column present', async () => {
49
- await createPublicItem('public_items_ownerfield', 'Alpha', 1)
50
- await createPublicItem('public_items_ownerfield', 'Beta', 2)
51
-
52
- // Query with auth
53
- const resultAuth = await api.resources.public_items_ownerfield.query({ simplified: false }, { auth: { userId: 1 } })
54
- validateJsonApiStructure(resultAuth, true)
55
- assert.equal(resultAuth.data.length, 2)
56
-
57
- // Query without auth (ownership disabled -> no filtering)
58
- const resultNoAuth = await api.resources.public_items_ownerfield.query({ simplified: false })
59
- validateJsonApiStructure(resultNoAuth, true)
60
- assert.equal(resultNoAuth.data.length, 2)
61
- })
62
-
63
- it('ownership:false - allows create+query without user_id column present', async () => {
64
- await createPublicItem('public_items_ownership', 'Gamma', 1)
65
- await createPublicItem('public_items_ownership', 'Delta', 2)
66
-
67
- const resultAuth = await api.resources.public_items_ownership.query({ simplified: false }, { auth: { userId: 99 } })
68
- validateJsonApiStructure(resultAuth, true)
69
- assert.equal(resultAuth.data.length, 2)
70
-
71
- const resultNoAuth = await api.resources.public_items_ownership.query({ simplified: false })
72
- validateJsonApiStructure(resultNoAuth, true)
73
- assert.equal(resultNoAuth.data.length, 2)
74
- })
75
- })
76
-