@opensaas/stack-core 0.20.1 → 0.22.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 (136) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +334 -0
  3. package/CLAUDE.md +29 -11
  4. package/dist/access/access-filter.d.ts +29 -0
  5. package/dist/access/access-filter.d.ts.map +1 -0
  6. package/dist/access/access-filter.js +68 -0
  7. package/dist/access/access-filter.js.map +1 -0
  8. package/dist/access/engine.d.ts +15 -48
  9. package/dist/access/engine.d.ts.map +1 -1
  10. package/dist/access/engine.js +14 -280
  11. package/dist/access/engine.js.map +1 -1
  12. package/dist/access/field-access.d.ts +44 -0
  13. package/dist/access/field-access.d.ts.map +1 -0
  14. package/dist/access/field-access.js +123 -0
  15. package/dist/access/field-access.js.map +1 -0
  16. package/dist/access/field-access.test.d.ts +2 -0
  17. package/dist/access/field-access.test.d.ts.map +1 -0
  18. package/dist/access/{engine.test.js → field-access.test.js} +2 -2
  19. package/dist/access/field-access.test.js.map +1 -0
  20. package/dist/access/field-visibility.d.ts +13 -0
  21. package/dist/access/field-visibility.d.ts.map +1 -0
  22. package/dist/access/field-visibility.js +178 -0
  23. package/dist/access/field-visibility.js.map +1 -0
  24. package/dist/access/index.d.ts +4 -1
  25. package/dist/access/index.d.ts.map +1 -1
  26. package/dist/access/index.js +8 -1
  27. package/dist/access/index.js.map +1 -1
  28. package/dist/access/multi-column-read-write.test.d.ts +2 -0
  29. package/dist/access/multi-column-read-write.test.d.ts.map +1 -0
  30. package/dist/access/multi-column-read-write.test.js +149 -0
  31. package/dist/access/multi-column-read-write.test.js.map +1 -0
  32. package/dist/config/index.d.ts +1 -1
  33. package/dist/config/index.d.ts.map +1 -1
  34. package/dist/config/types.d.ts +334 -5
  35. package/dist/config/types.d.ts.map +1 -1
  36. package/dist/context/hook-pipeline.d.ts +49 -0
  37. package/dist/context/hook-pipeline.d.ts.map +1 -0
  38. package/dist/context/hook-pipeline.js +75 -0
  39. package/dist/context/hook-pipeline.js.map +1 -0
  40. package/dist/context/index.d.ts.map +1 -1
  41. package/dist/context/index.js +30 -462
  42. package/dist/context/index.js.map +1 -1
  43. package/dist/context/nested-operations.d.ts.map +1 -1
  44. package/dist/context/nested-operations.js +72 -68
  45. package/dist/context/nested-operations.js.map +1 -1
  46. package/dist/context/write-pipeline.d.ts +158 -0
  47. package/dist/context/write-pipeline.d.ts.map +1 -0
  48. package/dist/context/write-pipeline.js +306 -0
  49. package/dist/context/write-pipeline.js.map +1 -0
  50. package/dist/extend.d.ts +3 -0
  51. package/dist/extend.d.ts.map +1 -0
  52. package/dist/extend.js +10 -0
  53. package/dist/extend.js.map +1 -0
  54. package/dist/fields/format-prisma-default.d.ts +35 -0
  55. package/dist/fields/format-prisma-default.d.ts.map +1 -0
  56. package/dist/fields/format-prisma-default.js +52 -0
  57. package/dist/fields/format-prisma-default.js.map +1 -0
  58. package/dist/fields/format-prisma-default.test.d.ts +2 -0
  59. package/dist/fields/format-prisma-default.test.d.ts.map +1 -0
  60. package/dist/fields/format-prisma-default.test.js +54 -0
  61. package/dist/fields/format-prisma-default.test.js.map +1 -0
  62. package/dist/fields/index.d.ts +1 -0
  63. package/dist/fields/index.d.ts.map +1 -1
  64. package/dist/fields/index.js +267 -18
  65. package/dist/fields/index.js.map +1 -1
  66. package/dist/fields/select.test.js +85 -0
  67. package/dist/fields/select.test.js.map +1 -1
  68. package/dist/fields/text-keystone-compat.test.d.ts +2 -0
  69. package/dist/fields/text-keystone-compat.test.d.ts.map +1 -0
  70. package/dist/fields/text-keystone-compat.test.js +93 -0
  71. package/dist/fields/text-keystone-compat.test.js.map +1 -0
  72. package/dist/hooks/index.d.ts +20 -0
  73. package/dist/hooks/index.d.ts.map +1 -1
  74. package/dist/hooks/index.js +246 -0
  75. package/dist/hooks/index.js.map +1 -1
  76. package/dist/index.d.ts +6 -8
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js +25 -9
  79. package/dist/index.js.map +1 -1
  80. package/dist/index.test.d.ts +2 -0
  81. package/dist/index.test.d.ts.map +1 -0
  82. package/dist/index.test.js +33 -0
  83. package/dist/index.test.js.map +1 -0
  84. package/dist/internal.d.ts +8 -0
  85. package/dist/internal.d.ts.map +1 -0
  86. package/dist/internal.js +16 -0
  87. package/dist/internal.js.map +1 -0
  88. package/dist/mcp/handler.js +0 -1
  89. package/dist/mcp/handler.js.map +1 -1
  90. package/dist/validation/field-config.d.ts +55 -0
  91. package/dist/validation/field-config.d.ts.map +1 -0
  92. package/dist/validation/field-config.js +100 -0
  93. package/dist/validation/field-config.js.map +1 -0
  94. package/dist/validation/field-config.test.d.ts +2 -0
  95. package/dist/validation/field-config.test.d.ts.map +1 -0
  96. package/dist/validation/field-config.test.js +159 -0
  97. package/dist/validation/field-config.test.js.map +1 -0
  98. package/package.json +11 -3
  99. package/src/access/access-filter.ts +97 -0
  100. package/src/access/engine.ts +13 -396
  101. package/src/access/{engine.test.ts → field-access.test.ts} +1 -1
  102. package/src/access/field-access.ts +159 -0
  103. package/src/access/field-visibility.ts +269 -0
  104. package/src/access/index.ts +7 -4
  105. package/src/access/multi-column-read-write.test.ts +255 -0
  106. package/src/config/index.ts +3 -0
  107. package/src/config/types.ts +342 -4
  108. package/src/context/hook-pipeline.ts +160 -0
  109. package/src/context/index.ts +29 -667
  110. package/src/context/nested-operations.ts +142 -111
  111. package/src/context/write-pipeline.ts +543 -0
  112. package/src/extend.ts +19 -0
  113. package/src/fields/format-prisma-default.test.ts +64 -0
  114. package/src/fields/format-prisma-default.ts +67 -0
  115. package/src/fields/index.ts +375 -20
  116. package/src/fields/select.test.ts +99 -0
  117. package/src/fields/text-keystone-compat.test.ts +126 -0
  118. package/src/hooks/index.ts +270 -0
  119. package/src/index.test.ts +50 -0
  120. package/src/index.ts +35 -82
  121. package/src/internal.ts +49 -0
  122. package/src/mcp/handler.ts +0 -2
  123. package/src/validation/field-config.test.ts +199 -0
  124. package/src/validation/field-config.ts +145 -0
  125. package/tests/access-relationships.test.ts +4 -4
  126. package/tests/access.test.ts +1 -1
  127. package/tests/field-hooks.test.ts +410 -0
  128. package/tests/field-types.test.ts +1 -1
  129. package/tests/hook-pipeline.test.ts +233 -0
  130. package/tests/nested-operation-registry.test.ts +206 -0
  131. package/tests/write-pipeline.test.ts +588 -0
  132. package/tsconfig.tsbuildinfo +1 -1
  133. package/vitest.config.ts +43 -1
  134. package/dist/access/engine.test.d.ts +0 -2
  135. package/dist/access/engine.test.d.ts.map +0 -1
  136. package/dist/access/engine.test.js.map +0 -1
@@ -1,3 +1,17 @@
1
+ /**
2
+ * Access engine — operation-level access control and shared helpers.
3
+ *
4
+ * This module holds the *operation-level* (list-level) access primitives and
5
+ * the ref-parsing helper shared across both phases of the two-phase read:
6
+ *
7
+ * - Phase 1, Access Filter (pre-query row/relation scoping): `access-filter.ts`
8
+ * - Phase 2, Field Visibility (post-query field stripping + resolveOutput +
9
+ * virtual fields): `field-visibility.ts`
10
+ *
11
+ * Field-level access evaluation is centralized in `field-access.ts`
12
+ * (`checkFieldAccess`). See `docs/adr/0001-access-control-is-a-two-phase-read.md`
13
+ * and the access-control glossary in `CONTEXT.md`.
14
+ */
1
15
  /**
2
16
  * Check if access control result is a boolean
3
17
  */
@@ -64,284 +78,4 @@ export function mergeFilters(userFilter, accessFilter) {
64
78
  AND: [accessFilter, userFilter],
65
79
  };
66
80
  }
67
- /**
68
- * Check field-level access for a specific operation
69
- */
70
- export async function checkFieldAccess(fieldAccess, operation, args) {
71
- // Skip access check in sudo mode
72
- if (args.context._isSudo) {
73
- return true;
74
- }
75
- if (!fieldAccess) {
76
- return true; // No field access means allow
77
- }
78
- const accessControl = fieldAccess[operation];
79
- if (!accessControl) {
80
- return true; // No specific access control means allow
81
- }
82
- const result = await accessControl({
83
- session: args.session,
84
- item: args.item,
85
- context: args.context,
86
- inputData: args.inputData,
87
- operation,
88
- });
89
- // If result is false, deny access
90
- if (result === false) {
91
- return false;
92
- }
93
- // If result is true, allow access
94
- if (result === true) {
95
- return true;
96
- }
97
- // Default to allowing access if we can't determine
98
- return true;
99
- }
100
- /**
101
- * Simple filter matching for field-level access
102
- * Checks if an item matches a Prisma-like filter object
103
- */
104
- function matchesFilter(item, filter) {
105
- for (const [key, condition] of Object.entries(filter)) {
106
- if (typeof condition === 'object' && condition !== null) {
107
- // Handle nested conditions like { equals: value }
108
- if ('equals' in condition) {
109
- if (item[key] !== condition.equals) {
110
- return false;
111
- }
112
- }
113
- else if ('not' in condition) {
114
- if (item[key] === condition.not) {
115
- return false;
116
- }
117
- }
118
- // Add more condition types as needed
119
- }
120
- else {
121
- // Direct equality check
122
- if (item[key] !== condition) {
123
- return false;
124
- }
125
- }
126
- }
127
- return true;
128
- }
129
- /**
130
- * Build Prisma include object with access control filters
131
- * This allows us to filter relationships at the database level instead of in memory
132
- */
133
- export async function buildIncludeWithAccessControl(fieldConfigs, args, config, depth = 0) {
134
- const MAX_DEPTH = 5;
135
- if (depth >= MAX_DEPTH) {
136
- return undefined;
137
- }
138
- // Skip auto-including relationships when inside a resolveOutput hook
139
- // This prevents infinite loops when hooks make DB queries that include
140
- // relationships back to the same entity (e.g., User virtual field queries Posts
141
- // which includes author back to User, triggering the virtual field again)
142
- if (args.context._resolveOutputCounter.depth > 0) {
143
- return undefined;
144
- }
145
- const include = {};
146
- let hasRelationships = false;
147
- for (const [fieldName, fieldConfig] of Object.entries(fieldConfigs)) {
148
- if (fieldConfig?.type === 'relationship' && 'ref' in fieldConfig && fieldConfig.ref) {
149
- hasRelationships = true;
150
- const relatedConfig = getRelatedListConfig(fieldConfig.ref, config);
151
- if (relatedConfig) {
152
- // Check query access for the related list
153
- const queryAccess = relatedConfig.listConfig.access?.operation?.query;
154
- const accessResult = await checkAccess(queryAccess, {
155
- session: args.session,
156
- context: args.context,
157
- });
158
- // If access is completely denied, exclude this relationship
159
- if (accessResult === false) {
160
- continue;
161
- }
162
- // Build the include entry
163
- const includeEntry = {};
164
- // If access returns a filter, add it to the where clause
165
- if (typeof accessResult === 'object') {
166
- includeEntry.where = accessResult;
167
- }
168
- // Recursively build nested includes
169
- const nestedInclude = await buildIncludeWithAccessControl(relatedConfig.listConfig.fields, args, config, depth + 1);
170
- if (nestedInclude && Object.keys(nestedInclude).length > 0) {
171
- includeEntry.include = nestedInclude;
172
- }
173
- // Add to include object
174
- include[fieldName] = Object.keys(includeEntry).length > 0 ? includeEntry : true;
175
- }
176
- }
177
- }
178
- return hasRelationships ? include : undefined;
179
- }
180
- /**
181
- * Filter fields from an object based on read access
182
- * Recursively applies access control to nested relationships
183
- */
184
- export async function filterReadableFields(item, fieldConfigs, args, config, depth = 0, listKey) {
185
- const filtered = {};
186
- const MAX_DEPTH = 5; // Prevent infinite recursion
187
- // Process existing fields from the database result
188
- for (const [fieldName, value] of Object.entries(item)) {
189
- const fieldConfig = fieldConfigs[fieldName];
190
- // Always include id, createdAt, updatedAt
191
- if (['id', 'createdAt', 'updatedAt'].includes(fieldName)) {
192
- filtered[fieldName] = value;
193
- continue;
194
- }
195
- // Check field access (checkFieldAccess already handles sudo mode)
196
- const canRead = await checkFieldAccess(fieldConfig?.access, 'read', {
197
- ...args,
198
- item,
199
- });
200
- if (!canRead) {
201
- continue;
202
- }
203
- // Handle relationship fields - recursively filter fields within related items
204
- // Note: Access control filtering is now done at database level via buildIncludeWithAccessControl
205
- // This only handles field-level access (hiding sensitive fields)
206
- if (config &&
207
- fieldConfig?.type === 'relationship' &&
208
- 'ref' in fieldConfig &&
209
- fieldConfig.ref &&
210
- value !== null &&
211
- value !== undefined &&
212
- depth < MAX_DEPTH) {
213
- const relatedConfig = getRelatedListConfig(fieldConfig.ref, config);
214
- if (relatedConfig) {
215
- // For many relationships (arrays) - recursively filter fields in each item
216
- // The recursive call already handles applying resolveOutput hooks
217
- if (Array.isArray(value)) {
218
- filtered[fieldName] = await Promise.all(value.map((relatedItem) => filterReadableFields(relatedItem, relatedConfig.listConfig.fields, args, config, depth + 1, relatedConfig.listName)));
219
- }
220
- // For single relationships (objects) - recursively filter fields
221
- // The recursive call already handles applying resolveOutput hooks
222
- else if (typeof value === 'object') {
223
- filtered[fieldName] = await filterReadableFields(value, relatedConfig.listConfig.fields, args, config, depth + 1, relatedConfig.listName);
224
- }
225
- }
226
- else {
227
- // Related config not found, include the value as-is
228
- filtered[fieldName] = value;
229
- }
230
- }
231
- else {
232
- // Non-relationship field or no config provided - apply resolveOutput hook if present
233
- if (fieldConfig?.hooks?.resolveOutput && listKey) {
234
- // Cast to runtime type for generic execution
235
- // At runtime, the hook will receive the correct value type for the field
236
- const hook = fieldConfig.hooks.resolveOutput;
237
- // Increment depth counter to prevent infinite loops from hooks making DB queries
238
- // that include relationships back to the same entity
239
- args.context._resolveOutputCounter.depth++;
240
- try {
241
- // Use Promise.resolve() to handle both sync and async hooks
242
- filtered[fieldName] = await Promise.resolve(hook({
243
- value,
244
- operation: 'query',
245
- fieldName,
246
- listKey,
247
- item,
248
- context: args.context,
249
- }));
250
- }
251
- finally {
252
- args.context._resolveOutputCounter.depth--;
253
- }
254
- }
255
- else {
256
- filtered[fieldName] = value;
257
- }
258
- }
259
- }
260
- // Process virtual fields - compute values from other fields
261
- // Virtual fields don't exist in the database result, so we need to compute them separately
262
- for (const [fieldName, fieldConfig] of Object.entries(fieldConfigs)) {
263
- // Skip if already processed (from database result)
264
- if (fieldName in filtered) {
265
- continue;
266
- }
267
- // Only process virtual fields
268
- if (!fieldConfig.virtual) {
269
- continue;
270
- }
271
- // Check field access
272
- const canRead = await checkFieldAccess(fieldConfig.access, 'read', {
273
- ...args,
274
- item,
275
- });
276
- if (!canRead) {
277
- continue;
278
- }
279
- // Virtual fields must have resolveOutput hook to compute their value
280
- if (fieldConfig.hooks?.resolveOutput && listKey) {
281
- const hook = fieldConfig.hooks.resolveOutput;
282
- // Increment depth counter to prevent infinite loops from hooks making DB queries
283
- // that include relationships back to the same entity
284
- args.context._resolveOutputCounter.depth++;
285
- try {
286
- // Use Promise.resolve() to handle both sync and async hooks
287
- filtered[fieldName] = await Promise.resolve(hook({
288
- value: undefined, // Virtual fields don't have a database value
289
- operation: 'query',
290
- fieldName,
291
- listKey,
292
- item: filtered, // Pass filtered item so virtual field can access other fields
293
- context: args.context,
294
- }));
295
- }
296
- finally {
297
- args.context._resolveOutputCounter.depth--;
298
- }
299
- }
300
- }
301
- return filtered;
302
- }
303
- /**
304
- * Filter fields from input data based on write access (create/update)
305
- */
306
- export async function filterWritableFields(data, fieldConfigs, operation, args) {
307
- const filtered = {};
308
- // Build a set of foreign key field names to exclude
309
- // Foreign keys should not be in the data when using Prisma's relation syntax
310
- const foreignKeyFields = new Set();
311
- for (const [fieldName, fieldConfig] of Object.entries(fieldConfigs)) {
312
- if (fieldConfig.type === 'relationship') {
313
- // For non-many relationships, Prisma creates a foreign key field named `${fieldName}Id`
314
- const relConfig = fieldConfig;
315
- if (!relConfig.many) {
316
- foreignKeyFields.add(`${fieldName}Id`);
317
- }
318
- }
319
- }
320
- for (const [fieldName, value] of Object.entries(data)) {
321
- const fieldConfig = fieldConfigs[fieldName];
322
- // Skip system fields
323
- if (['id', 'createdAt', 'updatedAt'].includes(fieldName)) {
324
- continue;
325
- }
326
- // Skip virtual fields - they don't store in database
327
- // Virtual fields with resolveInput hooks handle side effects separately
328
- if (fieldConfig && 'virtual' in fieldConfig && fieldConfig.virtual) {
329
- continue;
330
- }
331
- // Skip foreign key fields (e.g., authorId) when their corresponding relationship field exists
332
- // This prevents conflicts when using Prisma's relation syntax (e.g., author: { connect: { id } })
333
- if (foreignKeyFields.has(fieldName)) {
334
- continue;
335
- }
336
- // Check field access (checkFieldAccess already handles sudo mode)
337
- const canWrite = await checkFieldAccess(fieldConfig?.access, operation, {
338
- ...args,
339
- inputData: args.inputData,
340
- });
341
- if (canWrite) {
342
- filtered[fieldName] = value;
343
- }
344
- }
345
- return filtered;
346
- }
347
81
  //# sourceMappingURL=engine.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"engine.js","sourceRoot":"","sources":["../../src/access/engine.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"engine.js","sourceRoot":"","sources":["../../src/access/engine.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;GAaG;AAEH;;GAEG;AACH,MAAM,UAAU,SAAS,CAAC,KAAc;IACtC,OAAO,OAAO,KAAK,KAAK,SAAS,CAAA;AACnC,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,cAAc,CAAC,KAAc;IAC3C,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;AAC7E,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,oBAAoB,CAClC,eAAuB,EACvB,MAAsB;IAGtB,uDAAuD;IACvD,MAAM,KAAK,GAAG,eAAe,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IACxC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC7C,OAAO,IAAI,CAAA;IACb,CAAC;IAED,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,CAAC,CAAA;IACzB,MAAM,UAAU,GAAG,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAA;IAEzC,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,OAAO,IAAI,CAAA;IACb,CAAC;IAED,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAA;AACjC,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,aAA2C,EAC3C,IAIC;IAED,0CAA0C;IAC1C,IAAI,CAAC,aAAa,EAAE,CAAC;QACnB,OAAO,KAAK,CAAA;IACd,CAAC;IAED,sCAAsC;IACtC,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,IAAI,CAAC,CAAA;IAExC,OAAO,MAAM,CAAA;AACf,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,YAAY,CAC1B,UAAoC,EACpC,YAAoC;IAEpC,mCAAmC;IACnC,IAAI,YAAY,KAAK,KAAK,EAAE,CAAC;QAC3B,OAAO,IAAI,CAAA;IACb,CAAC;IAED,8CAA8C;IAC9C,IAAI,YAAY,KAAK,IAAI,EAAE,CAAC;QAC1B,OAAO,UAAU,IAAI,EAAE,CAAA;IACzB,CAAC;IAED,uCAAuC;IACvC,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,OAAO,YAAY,CAAA;IACrB,CAAC;IAED,2BAA2B;IAC3B,OAAO;QACL,GAAG,EAAE,CAAC,YAAY,EAAE,UAAU,CAAC;KAChC,CAAA;AACH,CAAC"}
@@ -0,0 +1,44 @@
1
+ import type { Session, AccessContext } from './types.js';
2
+ import type { FieldAccess } from './types.js';
3
+ /**
4
+ * Shared field-level access evaluation.
5
+ *
6
+ * This module is the single, canonical home for field-level access checks. Both
7
+ * read-time (Field Visibility, see `field-visibility.ts`) and write-time paths
8
+ * evaluate field access through `checkFieldAccess` — there is intentionally no
9
+ * second, parallel field-access evaluator. See
10
+ * `docs/adr/0001-access-control-is-a-two-phase-read.md` and the access-control
11
+ * glossary in `CONTEXT.md` for the two-phase read model that motivates this.
12
+ */
13
+ /**
14
+ * Check field-level access for a specific operation.
15
+ *
16
+ * This is the canonical field-access evaluator. Its signature is deliberate:
17
+ * field access can depend on the `operation`, on the already-fetched `item`
18
+ * (read/update/delete), and on the `inputData` being written (create/update),
19
+ * so all of those are accepted. Do not introduce a parallel evaluator with a
20
+ * narrower signature.
21
+ */
22
+ export declare function checkFieldAccess(fieldAccess: FieldAccess | undefined, operation: 'read' | 'create' | 'update', args: {
23
+ session: Session | null;
24
+ item?: Record<string, unknown>;
25
+ context: AccessContext & {
26
+ _isSudo?: boolean;
27
+ };
28
+ inputData?: Record<string, unknown>;
29
+ }): Promise<boolean>;
30
+ /**
31
+ * Filter fields from input data based on write access (create/update)
32
+ */
33
+ export declare function filterWritableFields<T extends Record<string, unknown>>(data: T, fieldConfigs: Record<string, {
34
+ access?: FieldAccess;
35
+ type?: string;
36
+ }>, operation: 'create' | 'update', args: {
37
+ session: Session | null;
38
+ item?: Record<string, unknown>;
39
+ context: AccessContext & {
40
+ _isSudo?: boolean;
41
+ };
42
+ inputData?: Record<string, unknown>;
43
+ }): Promise<Partial<T>>;
44
+ //# sourceMappingURL=field-access.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"field-access.d.ts","sourceRoot":"","sources":["../../src/access/field-access.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AACxD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAE7C;;;;;;;;;GASG;AAEH;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CACpC,WAAW,EAAE,WAAW,GAAG,SAAS,EACpC,SAAS,EAAE,MAAM,GAAG,QAAQ,GAAG,QAAQ,EACvC,IAAI,EAAE;IACJ,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;IACvB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,OAAO,EAAE,aAAa,GAAG;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAA;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACpC,GACA,OAAO,CAAC,OAAO,CAAC,CAmClB;AA8BD;;GAEG;AACH,wBAAsB,oBAAoB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC1E,IAAI,EAAE,CAAC,EACP,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,MAAM,CAAC,EAAE,WAAW,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,EACrE,SAAS,EAAE,QAAQ,GAAG,QAAQ,EAC9B,IAAI,EAAE;IACJ,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;IACvB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,OAAO,EAAE,aAAa,GAAG;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAA;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACpC,GACA,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAgDrB"}
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Shared field-level access evaluation.
3
+ *
4
+ * This module is the single, canonical home for field-level access checks. Both
5
+ * read-time (Field Visibility, see `field-visibility.ts`) and write-time paths
6
+ * evaluate field access through `checkFieldAccess` — there is intentionally no
7
+ * second, parallel field-access evaluator. See
8
+ * `docs/adr/0001-access-control-is-a-two-phase-read.md` and the access-control
9
+ * glossary in `CONTEXT.md` for the two-phase read model that motivates this.
10
+ */
11
+ /**
12
+ * Check field-level access for a specific operation.
13
+ *
14
+ * This is the canonical field-access evaluator. Its signature is deliberate:
15
+ * field access can depend on the `operation`, on the already-fetched `item`
16
+ * (read/update/delete), and on the `inputData` being written (create/update),
17
+ * so all of those are accepted. Do not introduce a parallel evaluator with a
18
+ * narrower signature.
19
+ */
20
+ export async function checkFieldAccess(fieldAccess, operation, args) {
21
+ // Skip access check in sudo mode
22
+ if (args.context._isSudo) {
23
+ return true;
24
+ }
25
+ if (!fieldAccess) {
26
+ return true; // No field access means allow
27
+ }
28
+ const accessControl = fieldAccess[operation];
29
+ if (!accessControl) {
30
+ return true; // No specific access control means allow
31
+ }
32
+ const result = await accessControl({
33
+ session: args.session,
34
+ item: args.item,
35
+ context: args.context,
36
+ inputData: args.inputData,
37
+ operation,
38
+ });
39
+ // If result is false, deny access
40
+ if (result === false) {
41
+ return false;
42
+ }
43
+ // If result is true, allow access
44
+ if (result === true) {
45
+ return true;
46
+ }
47
+ // Default to allowing access if we can't determine
48
+ return true;
49
+ }
50
+ /**
51
+ * Simple filter matching for field-level access
52
+ * Checks if an item matches a Prisma-like filter object
53
+ */
54
+ function matchesFilter(item, filter) {
55
+ for (const [key, condition] of Object.entries(filter)) {
56
+ if (typeof condition === 'object' && condition !== null) {
57
+ // Handle nested conditions like { equals: value }
58
+ if ('equals' in condition) {
59
+ if (item[key] !== condition.equals) {
60
+ return false;
61
+ }
62
+ }
63
+ else if ('not' in condition) {
64
+ if (item[key] === condition.not) {
65
+ return false;
66
+ }
67
+ }
68
+ // Add more condition types as needed
69
+ }
70
+ else {
71
+ // Direct equality check
72
+ if (item[key] !== condition) {
73
+ return false;
74
+ }
75
+ }
76
+ }
77
+ return true;
78
+ }
79
+ /**
80
+ * Filter fields from input data based on write access (create/update)
81
+ */
82
+ export async function filterWritableFields(data, fieldConfigs, operation, args) {
83
+ const filtered = {};
84
+ // Build a set of foreign key field names to exclude
85
+ // Foreign keys should not be in the data when using Prisma's relation syntax
86
+ const foreignKeyFields = new Set();
87
+ for (const [fieldName, fieldConfig] of Object.entries(fieldConfigs)) {
88
+ if (fieldConfig.type === 'relationship') {
89
+ // For non-many relationships, Prisma creates a foreign key field named `${fieldName}Id`
90
+ const relConfig = fieldConfig;
91
+ if (!relConfig.many) {
92
+ foreignKeyFields.add(`${fieldName}Id`);
93
+ }
94
+ }
95
+ }
96
+ for (const [fieldName, value] of Object.entries(data)) {
97
+ const fieldConfig = fieldConfigs[fieldName];
98
+ // Skip system fields
99
+ if (['id', 'createdAt', 'updatedAt'].includes(fieldName)) {
100
+ continue;
101
+ }
102
+ // Skip virtual fields - they don't store in database
103
+ // Virtual fields with resolveInput hooks handle side effects separately
104
+ if (fieldConfig && 'virtual' in fieldConfig && fieldConfig.virtual) {
105
+ continue;
106
+ }
107
+ // Skip foreign key fields (e.g., authorId) when their corresponding relationship field exists
108
+ // This prevents conflicts when using Prisma's relation syntax (e.g., author: { connect: { id } })
109
+ if (foreignKeyFields.has(fieldName)) {
110
+ continue;
111
+ }
112
+ // Check field access (checkFieldAccess already handles sudo mode)
113
+ const canWrite = await checkFieldAccess(fieldConfig?.access, operation, {
114
+ ...args,
115
+ inputData: args.inputData,
116
+ });
117
+ if (canWrite) {
118
+ filtered[fieldName] = value;
119
+ }
120
+ }
121
+ return filtered;
122
+ }
123
+ //# sourceMappingURL=field-access.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"field-access.js","sourceRoot":"","sources":["../../src/access/field-access.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=field-access.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"field-access.test.d.ts","sourceRoot":"","sources":["../../src/access/field-access.test.ts"],"names":[],"mappings":""}
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { filterWritableFields } from './engine.js';
2
+ import { filterWritableFields } from './field-access.js';
3
3
  describe('filterWritableFields', () => {
4
4
  it('should filter out foreign key fields when their corresponding relationship field exists', async () => {
5
5
  // Setup: Define field configs with a relationship field
@@ -126,4 +126,4 @@ describe('filterWritableFields', () => {
126
126
  expect(filtered).not.toHaveProperty('authorId');
127
127
  });
128
128
  });
129
- //# sourceMappingURL=engine.test.js.map
129
+ //# sourceMappingURL=field-access.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"field-access.test.js","sourceRoot":"","sources":["../../src/access/field-access.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ import type { Session, AccessContext } from './types.js';
2
+ import type { OpenSaasConfig, FieldConfig } from '../config/types.js';
3
+ /**
4
+ * Filter fields from an object based on read access
5
+ * Recursively applies access control to nested relationships
6
+ */
7
+ export declare function filterReadableFields<T extends Record<string, unknown>>(item: T, fieldConfigs: Record<string, FieldConfig>, args: {
8
+ session: Session | null;
9
+ context: AccessContext & {
10
+ _isSudo?: boolean;
11
+ };
12
+ }, config?: OpenSaasConfig, depth?: number, listKey?: string): Promise<Partial<T>>;
13
+ //# sourceMappingURL=field-visibility.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"field-visibility.d.ts","sourceRoot":"","sources":["../../src/access/field-visibility.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AACxD,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAqGrE;;;GAGG;AACH,wBAAsB,oBAAoB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC1E,IAAI,EAAE,CAAC,EACP,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,EACzC,IAAI,EAAE;IACJ,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;IACvB,OAAO,EAAE,aAAa,GAAG;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAA;CAC/C,EACD,MAAM,CAAC,EAAE,cAAc,EACvB,KAAK,GAAE,MAAU,EACjB,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAwJrB"}