qdadm 0.15.1 → 0.17.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 (66) hide show
  1. package/README.md +153 -1
  2. package/package.json +15 -2
  3. package/src/components/BoolCell.vue +11 -6
  4. package/src/components/forms/FormField.vue +64 -6
  5. package/src/components/forms/FormPage.vue +276 -0
  6. package/src/components/index.js +11 -0
  7. package/src/components/layout/AppLayout.vue +18 -9
  8. package/src/components/layout/BaseLayout.vue +183 -0
  9. package/src/components/layout/DashboardLayout.vue +100 -0
  10. package/src/components/layout/FormLayout.vue +261 -0
  11. package/src/components/layout/ListLayout.vue +334 -0
  12. package/src/components/layout/PageHeader.vue +6 -9
  13. package/src/components/layout/PageNav.vue +15 -0
  14. package/src/components/layout/Zone.vue +165 -0
  15. package/src/components/layout/defaults/DefaultBreadcrumb.vue +140 -0
  16. package/src/components/layout/defaults/DefaultFooter.vue +56 -0
  17. package/src/components/layout/defaults/DefaultFormActions.vue +53 -0
  18. package/src/components/layout/defaults/DefaultHeader.vue +69 -0
  19. package/src/components/layout/defaults/DefaultMenu.vue +197 -0
  20. package/src/components/layout/defaults/DefaultPagination.vue +79 -0
  21. package/src/components/layout/defaults/DefaultTable.vue +130 -0
  22. package/src/components/layout/defaults/DefaultToaster.vue +16 -0
  23. package/src/components/layout/defaults/DefaultUserInfo.vue +96 -0
  24. package/src/components/layout/defaults/index.js +17 -0
  25. package/src/composables/index.js +8 -6
  26. package/src/composables/useBreadcrumb.js +9 -5
  27. package/src/composables/useForm.js +135 -0
  28. package/src/composables/useFormPageBuilder.js +1154 -0
  29. package/src/composables/useHooks.js +53 -0
  30. package/src/composables/useLayoutResolver.js +260 -0
  31. package/src/composables/useListPageBuilder.js +336 -52
  32. package/src/composables/useNavContext.js +372 -0
  33. package/src/composables/useNavigation.js +38 -2
  34. package/src/composables/usePageTitle.js +59 -0
  35. package/src/composables/useSignals.js +49 -0
  36. package/src/composables/useZoneRegistry.js +162 -0
  37. package/src/core/bundles.js +406 -0
  38. package/src/core/decorator.js +322 -0
  39. package/src/core/extension.js +386 -0
  40. package/src/core/index.js +28 -0
  41. package/src/entity/EntityManager.js +314 -16
  42. package/src/entity/auth/AuthAdapter.js +125 -0
  43. package/src/entity/auth/PermissiveAdapter.js +64 -0
  44. package/src/entity/auth/index.js +11 -0
  45. package/src/entity/index.js +3 -0
  46. package/src/entity/storage/MockApiStorage.js +349 -0
  47. package/src/entity/storage/SdkStorage.js +478 -0
  48. package/src/entity/storage/index.js +2 -0
  49. package/src/hooks/HookRegistry.js +411 -0
  50. package/src/hooks/index.js +12 -0
  51. package/src/index.js +12 -0
  52. package/src/kernel/Kernel.js +141 -4
  53. package/src/kernel/SignalBus.js +180 -0
  54. package/src/kernel/index.js +7 -0
  55. package/src/module/moduleRegistry.js +124 -6
  56. package/src/orchestrator/Orchestrator.js +73 -1
  57. package/src/plugin.js +5 -0
  58. package/src/zones/ZoneRegistry.js +821 -0
  59. package/src/zones/index.js +16 -0
  60. package/src/zones/zones.js +189 -0
  61. package/src/composables/useEntityTitle.js +0 -121
  62. package/src/composables/useManager.js +0 -20
  63. package/src/composables/usePageBuilder.js +0 -334
  64. package/src/composables/useStatus.js +0 -146
  65. package/src/composables/useSubEditor.js +0 -165
  66. package/src/composables/useTabSync.js +0 -110
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Core Module
3
+ *
4
+ * Extension and helper utilities for qdadm modules.
5
+ */
6
+
7
+ export {
8
+ extendModule,
9
+ ExtensionBuilder,
10
+ } from './extension.js'
11
+
12
+ export {
13
+ createDecoratedManager,
14
+ withAuditLog as withAuditLogDecorator,
15
+ withSoftDelete as withSoftDeleteDecorator,
16
+ withTimestamps as withTimestampsDecorator,
17
+ withValidation,
18
+ } from './decorator.js'
19
+
20
+ export {
21
+ createHookBundle,
22
+ applyBundle,
23
+ applyBundles,
24
+ withSoftDelete,
25
+ withTimestamps,
26
+ withVersioning,
27
+ withAuditLog,
28
+ } from './bundles.js'
@@ -1,3 +1,6 @@
1
+ import { PermissiveAuthAdapter } from './auth/PermissiveAdapter.js'
2
+ import { AuthActions } from './auth/AuthAdapter.js'
3
+
1
4
  /**
2
5
  * EntityManager - Base class for entity CRUD operations
3
6
  *
@@ -59,7 +62,9 @@ export class EntityManager {
59
62
  // Relations
60
63
  children = {}, // { roles: { entity: 'roles', endpoint?: ':id/roles' } }
61
64
  parent = null, // { entity: 'users', foreignKey: 'user_id' }
62
- relations = {} // { groups: { entity: 'groups', through: 'user_groups' } }
65
+ relations = {}, // { groups: { entity: 'groups', through: 'user_groups' } }
66
+ // Auth adapter (for permission checks)
67
+ authAdapter = null // AuthAdapter instance or null (uses PermissiveAuthAdapter)
63
68
  } = options
64
69
 
65
70
  this.name = name
@@ -86,6 +91,12 @@ export class EntityManager {
86
91
  this._relations = relations
87
92
  this._orchestrator = null // Set when registered
88
93
 
94
+ // Auth adapter (fallback to permissive if not provided)
95
+ this._authAdapter = authAdapter
96
+
97
+ // HookRegistry reference for lifecycle hooks (set by Orchestrator)
98
+ this._hooks = null
99
+
89
100
  // Severity maps for status fields (field → value → severity)
90
101
  this._severityMaps = {}
91
102
 
@@ -97,6 +108,153 @@ export class EntityManager {
97
108
  valid: false // Is cache currently valid?
98
109
  }
99
110
  this._cacheLoading = null // Promise when cache is being loaded
111
+
112
+ // SignalBus reference for event emission (set by Orchestrator)
113
+ this._signals = null
114
+ }
115
+
116
+ // ============ SIGNALS ============
117
+
118
+ /**
119
+ * Set the SignalBus reference (called by Orchestrator during registration)
120
+ * @param {SignalBus} signals
121
+ */
122
+ setSignals(signals) {
123
+ this._signals = signals
124
+ }
125
+
126
+ /**
127
+ * Set the HookRegistry reference (called by Orchestrator during registration)
128
+ * @param {HookRegistry} hooks
129
+ */
130
+ setHooks(hooks) {
131
+ this._hooks = hooks
132
+ }
133
+
134
+ /**
135
+ * Get the HookRegistry reference
136
+ * @returns {HookRegistry|null}
137
+ */
138
+ get hooks() {
139
+ return this._hooks
140
+ }
141
+
142
+ /**
143
+ * Emit entity signals after CRUD operations
144
+ * Emits both entity-specific and generic signals via SignalBus.emitEntity
145
+ * @param {string} action - 'created', 'updated', or 'deleted'
146
+ * @param {object} data - Signal payload data { entity, id, ... }
147
+ * @private
148
+ */
149
+ _emitSignal(action, data) {
150
+ if (!this._signals) return
151
+ // Use SignalBus.emitEntity for proper dual-signal emission
152
+ this._signals.emitEntity(this.name, action, data)
153
+ }
154
+
155
+ // ============ LIFECYCLE HOOKS ============
156
+
157
+ /**
158
+ * Invoke a lifecycle hook for this entity
159
+ *
160
+ * Invokes both entity-specific hook (e.g., 'books:presave') and
161
+ * generic hook (e.g., 'entity:presave'). Entity-specific hooks run first.
162
+ *
163
+ * @param {string} hookName - Hook name without entity prefix (e.g., 'presave')
164
+ * @param {object} context - Hook context passed to handlers
165
+ * @private
166
+ */
167
+ async _invokeHook(hookName, context) {
168
+ if (!this._hooks) return
169
+
170
+ // Invoke entity-specific hook first (e.g., 'books:presave')
171
+ await this._hooks.invoke(`${this.name}:${hookName}`, context)
172
+
173
+ // Invoke generic hook (e.g., 'entity:presave')
174
+ await this._hooks.invoke(`entity:${hookName}`, context)
175
+ }
176
+
177
+ /**
178
+ * Build hook context for presave operations
179
+ *
180
+ * @typedef {object} PresaveContext
181
+ * @property {string} entity - Entity name
182
+ * @property {object} record - Record data (can be mutated by handlers)
183
+ * @property {boolean} isNew - True for create, false for update
184
+ * @property {string|number} [id] - Record ID (only for update)
185
+ * @property {EntityManager} manager - This manager instance
186
+ *
187
+ * @param {object} data - Record data
188
+ * @param {boolean} isNew - True for create, false for update
189
+ * @param {string|number} [id] - Record ID (only for update)
190
+ * @returns {PresaveContext}
191
+ * @private
192
+ */
193
+ _buildPresaveContext(data, isNew, id = null) {
194
+ const context = {
195
+ entity: this.name,
196
+ record: data,
197
+ isNew,
198
+ manager: this
199
+ }
200
+ if (!isNew && id !== null) {
201
+ context.id = id
202
+ }
203
+ return context
204
+ }
205
+
206
+ /**
207
+ * Build hook context for postsave operations
208
+ *
209
+ * @typedef {object} PostsaveContext
210
+ * @property {string} entity - Entity name
211
+ * @property {object} record - Original record data
212
+ * @property {object} result - Saved entity returned from storage
213
+ * @property {boolean} isNew - True for create, false for update
214
+ * @property {string|number} [id] - Record ID
215
+ * @property {EntityManager} manager - This manager instance
216
+ *
217
+ * @param {object} data - Original record data
218
+ * @param {object} result - Saved entity from storage
219
+ * @param {boolean} isNew - True for create, false for update
220
+ * @param {string|number} [id] - Record ID
221
+ * @returns {PostsaveContext}
222
+ * @private
223
+ */
224
+ _buildPostsaveContext(data, result, isNew, id = null) {
225
+ const context = {
226
+ entity: this.name,
227
+ record: data,
228
+ result,
229
+ isNew,
230
+ manager: this
231
+ }
232
+ if (id !== null) {
233
+ context.id = id
234
+ } else if (result?.[this.idField]) {
235
+ context.id = result[this.idField]
236
+ }
237
+ return context
238
+ }
239
+
240
+ /**
241
+ * Build hook context for predelete operations
242
+ *
243
+ * @typedef {object} PredeleteContext
244
+ * @property {string} entity - Entity name
245
+ * @property {string|number} id - Record ID to be deleted
246
+ * @property {EntityManager} manager - This manager instance
247
+ *
248
+ * @param {string|number} id - Record ID to delete
249
+ * @returns {PredeleteContext}
250
+ * @private
251
+ */
252
+ _buildPredeleteContext(id) {
253
+ return {
254
+ entity: this.name,
255
+ id,
256
+ manager: this
257
+ }
100
258
  }
101
259
 
102
260
  // ============ METADATA ACCESSORS ============
@@ -159,9 +317,71 @@ export class EntityManager {
159
317
 
160
318
  // ============ PERMISSIONS ============
161
319
 
320
+ /**
321
+ * Get the auth adapter (lazy-initialized PermissiveAuthAdapter if not set)
322
+ * @returns {AuthAdapter}
323
+ */
324
+ get authAdapter() {
325
+ if (!this._authAdapter) {
326
+ this._authAdapter = new PermissiveAuthAdapter()
327
+ }
328
+ return this._authAdapter
329
+ }
330
+
331
+ /**
332
+ * Set the auth adapter
333
+ * @param {AuthAdapter|null} adapter
334
+ */
335
+ set authAdapter(adapter) {
336
+ this._authAdapter = adapter
337
+ }
338
+
339
+ /**
340
+ * Check if the current user can perform an action, optionally on a specific record
341
+ *
342
+ * This is the primary permission check method. It combines:
343
+ * 1. Local restrictions (readOnly, scopeWhitelist)
344
+ * 2. Scope check via AuthAdapter.canPerform() - can user do this action type?
345
+ * 3. Silo check via AuthAdapter.canAccessRecord() - can user access this record?
346
+ *
347
+ * @param {string} action - Action to check: 'read', 'create', 'update', 'delete', 'list'
348
+ * @param {object} [record] - Optional: specific record to check (for silo validation)
349
+ * @returns {boolean} - true if action is allowed
350
+ *
351
+ * @example
352
+ * // Scope-only checks (no record)
353
+ * manager.canAccess('create') // Can user create new items?
354
+ * manager.canAccess('list') // Can user see the list?
355
+ *
356
+ * @example
357
+ * // Scope + silo checks (with record)
358
+ * manager.canAccess('read', item) // Can user see this specific item?
359
+ * manager.canAccess('update', item) // Can user edit this specific item?
360
+ * manager.canAccess('delete', item) // Can user delete this specific item?
361
+ */
362
+ canAccess(action, record = null) {
363
+ // 1. Check readOnly restriction for write actions
364
+ if (this._readOnly && action !== AuthActions.READ && action !== AuthActions.LIST) {
365
+ return false
366
+ }
367
+
368
+ // 2. Scope check: can user perform this action on this entity type?
369
+ const canPerformAction = this.authAdapter.canPerform(this.name, action)
370
+ if (!canPerformAction) {
371
+ return false
372
+ }
373
+
374
+ // 3. Silo check: if record provided, can user access this specific record?
375
+ if (record !== null) {
376
+ return this.authAdapter.canAccessRecord(this.name, record)
377
+ }
378
+
379
+ return true
380
+ }
381
+
162
382
  /**
163
383
  * Check if user can read entities
164
- * Override in subclass or provide via options to implement custom logic
384
+ * Delegates to canAccess('read', entity)
165
385
  *
166
386
  * @param {object} [entity] - Optional: specific entity to check
167
387
  * @returns {boolean} - true if user can read
@@ -170,8 +390,7 @@ export class EntityManager {
170
390
  * With entity: specific read permission (e.g., can see this item)
171
391
  */
172
392
  canRead(entity = null) {
173
- // Default: allow all reads
174
- return true
393
+ return this.canAccess(AuthActions.READ, entity)
175
394
  }
176
395
 
177
396
  /**
@@ -184,18 +403,17 @@ export class EntityManager {
184
403
 
185
404
  /**
186
405
  * Check if user can create new entities
187
- * Override in subclass to implement custom logic
406
+ * Delegates to canAccess('create')
188
407
  *
189
408
  * @returns {boolean} - true if user can create
190
409
  */
191
410
  canCreate() {
192
- if (this._readOnly) return false
193
- return true
411
+ return this.canAccess(AuthActions.CREATE)
194
412
  }
195
413
 
196
414
  /**
197
415
  * Check if user can update entities
198
- * Override in subclass to implement custom logic
416
+ * Delegates to canAccess('update', entity)
199
417
  *
200
418
  * @param {object} [entity] - Optional: specific entity to check
201
419
  * @returns {boolean} - true if user can update
@@ -204,20 +422,28 @@ export class EntityManager {
204
422
  * With entity: specific update permission (e.g., can edit this item)
205
423
  */
206
424
  canUpdate(entity = null) {
207
- if (this._readOnly) return false
208
- return true
425
+ return this.canAccess(AuthActions.UPDATE, entity)
209
426
  }
210
427
 
211
428
  /**
212
429
  * Check if user can delete entities
213
- * Override in subclass or provide via options to implement custom logic
430
+ * Delegates to canAccess('delete', entity)
214
431
  *
215
432
  * @param {object} [entity] - Optional: specific entity to check
216
433
  * @returns {boolean} - true if user can delete
217
434
  */
218
435
  canDelete(entity = null) {
219
- if (this._readOnly) return false
220
- return true
436
+ return this.canAccess(AuthActions.DELETE, entity)
437
+ }
438
+
439
+ /**
440
+ * Check if user can list entities
441
+ * Delegates to canAccess('list')
442
+ *
443
+ * @returns {boolean} - true if user can list
444
+ */
445
+ canList() {
446
+ return this.canAccess(AuthActions.LIST)
221
447
  }
222
448
 
223
449
  /**
@@ -465,13 +691,33 @@ export class EntityManager {
465
691
 
466
692
  /**
467
693
  * Create a new entity
694
+ *
695
+ * Lifecycle hooks invoked:
696
+ * - presave: Before storage.create(), can modify data or throw to abort
697
+ * - postsave: After successful storage.create(), for side effects
698
+ *
468
699
  * @param {object} data
469
700
  * @returns {Promise<object>} - The created entity
470
701
  */
471
702
  async create(data) {
472
703
  if (this.storage) {
473
- const result = await this.storage.create(data)
704
+ // Invoke presave hooks (can modify data or throw to abort)
705
+ const presaveContext = this._buildPresaveContext(data, true)
706
+ await this._invokeHook('presave', presaveContext)
707
+
708
+ // Use potentially modified data from context
709
+ const result = await this.storage.create(presaveContext.record)
474
710
  this.invalidateCache()
711
+
712
+ // Invoke postsave hooks (for side effects)
713
+ const postsaveContext = this._buildPostsaveContext(data, result, true)
714
+ await this._invokeHook('postsave', postsaveContext)
715
+
716
+ this._emitSignal('created', {
717
+ entity: result,
718
+ manager: this.name,
719
+ id: result?.[this.idField]
720
+ })
475
721
  return result
476
722
  }
477
723
  throw new Error(`[EntityManager:${this.name}] create() not implemented`)
@@ -479,14 +725,34 @@ export class EntityManager {
479
725
 
480
726
  /**
481
727
  * Update an entity (PUT - full replacement)
728
+ *
729
+ * Lifecycle hooks invoked:
730
+ * - presave: Before storage.update(), can modify data or throw to abort
731
+ * - postsave: After successful storage.update(), for side effects
732
+ *
482
733
  * @param {string|number} id
483
734
  * @param {object} data
484
735
  * @returns {Promise<object>}
485
736
  */
486
737
  async update(id, data) {
487
738
  if (this.storage) {
488
- const result = await this.storage.update(id, data)
739
+ // Invoke presave hooks (can modify data or throw to abort)
740
+ const presaveContext = this._buildPresaveContext(data, false, id)
741
+ await this._invokeHook('presave', presaveContext)
742
+
743
+ // Use potentially modified data from context
744
+ const result = await this.storage.update(id, presaveContext.record)
489
745
  this.invalidateCache()
746
+
747
+ // Invoke postsave hooks (for side effects)
748
+ const postsaveContext = this._buildPostsaveContext(data, result, false, id)
749
+ await this._invokeHook('postsave', postsaveContext)
750
+
751
+ this._emitSignal('updated', {
752
+ entity: result,
753
+ manager: this.name,
754
+ id
755
+ })
490
756
  return result
491
757
  }
492
758
  throw new Error(`[EntityManager:${this.name}] update() not implemented`)
@@ -494,14 +760,34 @@ export class EntityManager {
494
760
 
495
761
  /**
496
762
  * Partially update an entity (PATCH)
763
+ *
764
+ * Lifecycle hooks invoked:
765
+ * - presave: Before storage.patch(), can modify data or throw to abort
766
+ * - postsave: After successful storage.patch(), for side effects
767
+ *
497
768
  * @param {string|number} id
498
769
  * @param {object} data
499
770
  * @returns {Promise<object>}
500
771
  */
501
772
  async patch(id, data) {
502
773
  if (this.storage) {
503
- const result = await this.storage.patch(id, data)
774
+ // Invoke presave hooks (can modify data or throw to abort)
775
+ const presaveContext = this._buildPresaveContext(data, false, id)
776
+ await this._invokeHook('presave', presaveContext)
777
+
778
+ // Use potentially modified data from context
779
+ const result = await this.storage.patch(id, presaveContext.record)
504
780
  this.invalidateCache()
781
+
782
+ // Invoke postsave hooks (for side effects)
783
+ const postsaveContext = this._buildPostsaveContext(data, result, false, id)
784
+ await this._invokeHook('postsave', postsaveContext)
785
+
786
+ this._emitSignal('updated', {
787
+ entity: result,
788
+ manager: this.name,
789
+ id
790
+ })
505
791
  return result
506
792
  }
507
793
  throw new Error(`[EntityManager:${this.name}] patch() not implemented`)
@@ -509,13 +795,25 @@ export class EntityManager {
509
795
 
510
796
  /**
511
797
  * Delete an entity
798
+ *
799
+ * Lifecycle hooks invoked:
800
+ * - predelete: Before storage.delete(), can throw to abort (for cascade checks)
801
+ *
512
802
  * @param {string|number} id
513
803
  * @returns {Promise<void>}
514
804
  */
515
805
  async delete(id) {
516
806
  if (this.storage) {
807
+ // Invoke predelete hooks (can throw to abort, e.g., for cascade checks)
808
+ const predeleteContext = this._buildPredeleteContext(id)
809
+ await this._invokeHook('predelete', predeleteContext)
810
+
517
811
  const result = await this.storage.delete(id)
518
812
  this.invalidateCache()
813
+ this._emitSignal('deleted', {
814
+ manager: this.name,
815
+ id
816
+ })
519
817
  return result
520
818
  }
521
819
  throw new Error(`[EntityManager:${this.name}] delete() not implemented`)
@@ -0,0 +1,125 @@
1
+ /**
2
+ * AuthAdapter - Interface for scope and silo permission checks
3
+ *
4
+ * Applications implement this interface to plug their authentication/authorization
5
+ * system into qdadm's EntityManager. The adapter provides two levels of permission checks:
6
+ *
7
+ * 1. **Scopes** (action-level): Can the user perform this action on this entity type?
8
+ * Example: Can user create invoices? Can user delete users?
9
+ *
10
+ * 2. **Silos** (record-level): Can the user access this specific record?
11
+ * Example: Can user see invoice #123? (ownership, team membership, etc.)
12
+ *
13
+ * Usage:
14
+ * ```js
15
+ * class MyAuthAdapter extends AuthAdapter {
16
+ * canPerform(entity, action) {
17
+ * const user = this.getCurrentUser()
18
+ * return user?.permissions?.includes(`${entity}:${action}`)
19
+ * }
20
+ *
21
+ * canAccessRecord(entity, record) {
22
+ * const user = this.getCurrentUser()
23
+ * // Check ownership or team membership
24
+ * return record.owner_id === user?.id || record.team_id === user?.team_id
25
+ * }
26
+ *
27
+ * getCurrentUser() {
28
+ * return this._userStore.currentUser
29
+ * }
30
+ * }
31
+ * ```
32
+ *
33
+ * @interface
34
+ */
35
+ export class AuthAdapter {
36
+ /**
37
+ * Check if the current user can perform an action on an entity type (scope check)
38
+ *
39
+ * This is the coarse-grained permission check. It determines if the user has
40
+ * the right to perform a category of actions, regardless of which specific
41
+ * record they want to act on.
42
+ *
43
+ * @param {string} entity - Entity name (e.g., 'users', 'invoices', 'products')
44
+ * @param {string} action - Action to check: 'read', 'create', 'update', 'delete', 'list'
45
+ * @returns {boolean} True if user can perform the action on this entity type
46
+ *
47
+ * @example
48
+ * // Check if user can create invoices
49
+ * adapter.canPerform('invoices', 'create') // true/false
50
+ *
51
+ * // Check if user can delete users
52
+ * adapter.canPerform('users', 'delete') // true/false
53
+ */
54
+ canPerform(entity, action) {
55
+ throw new Error('[AuthAdapter] canPerform() must be implemented by subclass')
56
+ }
57
+
58
+ /**
59
+ * Check if the current user can access a specific record (silo check)
60
+ *
61
+ * This is the fine-grained permission check. After the scope check passes,
62
+ * this determines if the user can access a particular record based on
63
+ * ownership, team membership, or other business rules.
64
+ *
65
+ * Called during:
66
+ * - get() operations
67
+ * - update() / patch() operations
68
+ * - delete() operations
69
+ * - list() result filtering (optional)
70
+ *
71
+ * @param {string} entity - Entity name (e.g., 'users', 'invoices')
72
+ * @param {object} record - The full entity record to check access for
73
+ * @returns {boolean} True if user can access this specific record
74
+ *
75
+ * @example
76
+ * // Check if user can access a specific invoice
77
+ * adapter.canAccessRecord('invoices', { id: 123, owner_id: 456, ... })
78
+ *
79
+ * @example
80
+ * // Common implementations:
81
+ * // 1. Ownership: record.owner_id === user.id
82
+ * // 2. Team: record.team_id === user.team_id
83
+ * // 3. Role: user.role === 'admin' (admins see all)
84
+ * // 4. Hierarchical: record.organization_id in user.organizations
85
+ */
86
+ canAccessRecord(entity, record) {
87
+ throw new Error('[AuthAdapter] canAccessRecord() must be implemented by subclass')
88
+ }
89
+
90
+ /**
91
+ * Get the current authenticated user
92
+ *
93
+ * Returns the user object or null if not authenticated. The user object
94
+ * should contain whatever information is needed for permission checks
95
+ * (id, role, team_id, permissions array, etc.).
96
+ *
97
+ * @returns {object|null} Current user object or null if not authenticated
98
+ *
99
+ * @example
100
+ * // Typical user object:
101
+ * {
102
+ * id: 123,
103
+ * email: 'user@example.com',
104
+ * role: 'manager',
105
+ * team_id: 456,
106
+ * permissions: ['invoices:create', 'invoices:read', 'users:read']
107
+ * }
108
+ */
109
+ getCurrentUser() {
110
+ throw new Error('[AuthAdapter] getCurrentUser() must be implemented by subclass')
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Valid action types for scope checks
116
+ * @readonly
117
+ * @enum {string}
118
+ */
119
+ export const AuthActions = Object.freeze({
120
+ READ: 'read',
121
+ CREATE: 'create',
122
+ UPDATE: 'update',
123
+ DELETE: 'delete',
124
+ LIST: 'list'
125
+ })
@@ -0,0 +1,64 @@
1
+ /**
2
+ * PermissiveAuthAdapter - Default adapter that allows all operations
3
+ *
4
+ * This adapter is used when no custom AuthAdapter is provided to EntityManager.
5
+ * It returns `true` for all permission checks, effectively disabling authorization.
6
+ *
7
+ * Use cases:
8
+ * - Development/prototyping without auth setup
9
+ * - Backward compatibility with existing apps that don't use auth
10
+ * - Internal admin tools with implicit trust
11
+ * - Testing environments
12
+ *
13
+ * @example
14
+ * // Explicit usage (rarely needed)
15
+ * const adapter = new PermissiveAuthAdapter()
16
+ *
17
+ * adapter.canPerform('users', 'delete') // true
18
+ * adapter.canAccessRecord('invoices', { id: 123, secret: true }) // true
19
+ * adapter.getCurrentUser() // null
20
+ *
21
+ * @extends AuthAdapter
22
+ */
23
+ import { AuthAdapter } from './AuthAdapter.js'
24
+
25
+ export class PermissiveAuthAdapter extends AuthAdapter {
26
+ /**
27
+ * Always allows any action on any entity type
28
+ *
29
+ * @param {string} entity - Entity name (ignored)
30
+ * @param {string} action - Action to check (ignored)
31
+ * @returns {boolean} Always returns true
32
+ */
33
+ canPerform(entity, action) {
34
+ return true
35
+ }
36
+
37
+ /**
38
+ * Always allows access to any record
39
+ *
40
+ * @param {string} entity - Entity name (ignored)
41
+ * @param {object} record - The record (ignored)
42
+ * @returns {boolean} Always returns true
43
+ */
44
+ canAccessRecord(entity, record) {
45
+ return true
46
+ }
47
+
48
+ /**
49
+ * Returns null (no authenticated user in permissive mode)
50
+ *
51
+ * @returns {null} Always returns null
52
+ */
53
+ getCurrentUser() {
54
+ return null
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Factory function to create a PermissiveAuthAdapter
60
+ * @returns {PermissiveAuthAdapter}
61
+ */
62
+ export function createPermissiveAdapter() {
63
+ return new PermissiveAuthAdapter()
64
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Auth Module
3
+ *
4
+ * AuthAdapter interface and implementations for scope/silo permission checks.
5
+ */
6
+
7
+ // Interface
8
+ export { AuthAdapter, AuthActions } from './AuthAdapter.js'
9
+
10
+ // Implementations
11
+ export { PermissiveAuthAdapter, createPermissiveAdapter } from './PermissiveAdapter.js'
@@ -9,3 +9,6 @@ export { EntityManager, createEntityManager } from './EntityManager.js'
9
9
 
10
10
  // Storage adapters
11
11
  export * from './storage/index.js'
12
+
13
+ // Auth adapters
14
+ export * from './auth/index.js'