@object-ui/core 17.7.0 → 18.0.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/CHANGELOG.md +650 -0
  2. package/README.md +127 -0
  3. package/dist/actions/ActionRunner.d.ts +192 -49
  4. package/dist/actions/ActionRunner.js +389 -102
  5. package/dist/actions/UndoManager.d.ts +8 -13
  6. package/dist/actions/actionKeys.d.ts +1 -1
  7. package/dist/actions/actionKeys.js +10 -0
  8. package/dist/actions/index.d.ts +1 -0
  9. package/dist/actions/index.js +1 -0
  10. package/dist/actions/navigationTarget.d.ts +41 -0
  11. package/dist/actions/navigationTarget.js +20 -0
  12. package/dist/actions/recordDelete.d.ts +59 -23
  13. package/dist/actions/recordDelete.js +40 -42
  14. package/dist/adapters/ApiDataSource.d.ts +8 -4
  15. package/dist/adapters/ApiDataSource.js +19 -7
  16. package/dist/adapters/resolveDataSource.d.ts +9 -6
  17. package/dist/adapters/resolveDataSource.js +9 -12
  18. package/dist/data-scope/ViewDataProvider.d.ts +7 -24
  19. package/dist/data-scope/ViewDataProvider.js +6 -53
  20. package/dist/evaluator/deadlineSettled.d.ts +40 -0
  21. package/dist/evaluator/deadlineSettled.js +45 -0
  22. package/dist/evaluator/fieldRules.js +6 -1
  23. package/dist/evaluator/index.d.ts +1 -0
  24. package/dist/evaluator/index.js +1 -0
  25. package/dist/evaluator/optionRules.d.ts +1 -1
  26. package/dist/evaluator/optionRules.js +1 -1
  27. package/dist/evaluator/subjectPermissions.d.ts +30 -1
  28. package/dist/evaluator/subjectPermissions.js +42 -0
  29. package/dist/index.d.ts +1 -0
  30. package/dist/index.js +3 -0
  31. package/dist/registry/Registry.d.ts +34 -13
  32. package/dist/registry/Registry.js +5 -22
  33. package/dist/utils/affordanceGrants.d.ts +371 -0
  34. package/dist/utils/affordanceGrants.js +252 -0
  35. package/dist/utils/chart-series.d.ts +34 -0
  36. package/dist/utils/chart-series.js +25 -2
  37. package/dist/utils/date-display.d.ts +42 -50
  38. package/dist/utils/date-display.js +192 -11
  39. package/dist/utils/expand-fields.d.ts +6 -4
  40. package/dist/utils/expand-fields.js +6 -4
  41. package/dist/utils/filter-converter.d.ts +6 -2
  42. package/dist/utils/filter-converter.js +6 -2
  43. package/dist/utils/normalize-list-view.d.ts +16 -12
  44. package/dist/utils/normalize-list-view.js +66 -67
  45. package/dist/utils/predicate-fields.js +6 -2
  46. package/dist/utils/record-source.d.ts +1 -1
  47. package/dist/utils/record-source.js +1 -1
  48. package/dist/utils/reference-keys.js +1 -1
  49. package/dist/validation/schema-validator.js +29 -5
  50. package/dist/validation/server-owned-value.d.ts +5 -1
  51. package/dist/validation/server-owned-value.js +5 -1
  52. package/package.json +4 -4
@@ -0,0 +1,371 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ /**
9
+ * The affordance-to-grant map (objectui#12082): ONE table naming, for every
10
+ * console affordance that offers a write, the grant the caller must hold for
11
+ * it — and ONE resolver every such affordance reads.
12
+ *
13
+ * ## The family this closes
14
+ *
15
+ * Each console surface used to decide on its own which grant its affordance
16
+ * reads, and each surface that decided wrongly was found and fixed alone
17
+ * (objectui#4296, #10107, #11000, #12047). objectui#12082 is the member that
18
+ * hurt most: a create form gated its fields on the EDIT grant, so a
19
+ * create-only role could not fill the form it was allowed to submit. The same
20
+ * pass found the record header not reading the update grant at all, and a
21
+ * lookup offering "Create new" with no grant read either (objectui#12081). The
22
+ * cause is one shape — an affordance spelling its own permission logic — so
23
+ * the fix is one table every affordance reads, not another one-off repair.
24
+ *
25
+ * ## What a row says
26
+ *
27
+ * `crud` — the resolved CRUD-affordance bit the affordance also needs: the
28
+ * object's managed-object policy (ADR-0103, the spec's
29
+ * `resolveCrudAffordances`) intersected with the server's effective API
30
+ * operation set (`/me/permissions` `apiOperations`, objectstack#3391), both
31
+ * through {@link resolveEffectiveCrudAffordances}. `null` for an affordance
32
+ * whose write does not go through the object's generic data door (an
33
+ * attachment upload goes through the storage route; the import template has
34
+ * its own endpoint), so that door's operation set says nothing about it.
35
+ *
36
+ * `grant` — the caller's object grant it exercises, asked as `can(object,
37
+ * grant)` (`MePermissionsProvider` maps `create` → `allowCreate`, `update` →
38
+ * `allowEdit`, `delete` → `allowDelete`).
39
+ *
40
+ * `field` — for an affordance that offers FIELDS to write, the field-level
41
+ * question asked of each one: `create` for an insert, `write` for an update.
42
+ * They differ only for a field the permission set does not mention, which the
43
+ * server's field step lets through so that object admission decides: on an
44
+ * insert that is `allowCreate`, on an update `allowEdit`. A field the set marks
45
+ * `editable: false` is refused on both, and the resolver answers that the same
46
+ * way for both questions. So a create form follows what the server enforces on
47
+ * insert, and adds no rule the server does not have.
48
+ *
49
+ * The `write` question is the server's whole update rule for the field, so it
50
+ * also carries the one field whose update needs an OBJECT grant beyond
51
+ * `allowEdit`: the record's owner, the field the spec's `SystemFieldName`
52
+ * constant names `OWNER_ID`. The server refuses an update that writes it without
53
+ * the transfer grant (`allowTransfer`, or `modifyAllRecords`), so the resolver
54
+ * answers `write` for it with that grant too (objectui#12103). Every row that
55
+ * asks `write` therefore offers the owner field only to a caller who may
56
+ * transfer the record — the in-place edit, the list's in-cell edit and an edit
57
+ * form alike, with no row of its own to forget.
58
+ *
59
+ * ## The verdict
60
+ *
61
+ * An affordance shows when the managed-object policy, the effective API
62
+ * operation set AND the caller's grant all allow it ({@link resolveAffordance}).
63
+ * Its `userActions` predicates (the #2614 object form) are surfaced only then:
64
+ * a predicate narrows, and never re-opens what policy, operation set or grant
65
+ * closed.
66
+ *
67
+ * ## Fail-open, as before
68
+ *
69
+ * With no permission provider mounted, `usePermissions()` answers `can` with
70
+ * `true` and `isLoaded` with `false`, so every grant here reads open and the
71
+ * field question is not asked at all — a standalone embed, a designer preview
72
+ * and a public surface behave as they did before permissions existed, and the
73
+ * server still enforces (the `fieldWriteGate.ts` contract in
74
+ * `@object-ui/plugin-form`). The policy half is not a per-principal answer and
75
+ * does not fail open with it, exactly as `resolveEffectiveCrudAffordances`
76
+ * never did.
77
+ *
78
+ * ## Adding an affordance
79
+ *
80
+ * Add its row here and read it through {@link resolveAffordance} (or
81
+ * {@link resolveFieldAffordance} for fields). The enumeration pin in
82
+ * `@object-ui/plugin-form` (`affordanceGrantMap-12082.test.tsx`) runs every row
83
+ * against four grant shapes and holds the row set to its own expectation table,
84
+ * and its census refuses a console source file that reads a CRUD grant without
85
+ * this map.
86
+ *
87
+ * An affordance that reads NO grant is invisible to a census of grant reads —
88
+ * the calendar's quick-create and drag-to-reschedule and the kanban card move
89
+ * were such affordances until they gained their rows. So the same pin also
90
+ * counts the WRITES: every call site that invokes a create, update or delete on
91
+ * a data source has an entry there naming the row it sits behind and the file
92
+ * that reads it, or the reason it has none. A write site added without an entry
93
+ * turns that pin red.
94
+ */
95
+ import { type RowCrudPredicates, type SchemaLike } from './managedBy.js';
96
+ /** The caller's object grant an affordance exercises. */
97
+ export type AffordanceGrant = 'create' | 'update' | 'delete';
98
+ /**
99
+ * The field-level question a field affordance asks: `create` follows the
100
+ * server's insert rule, `write` its update rule.
101
+ */
102
+ export type FieldAffordanceGrant = 'create' | 'write';
103
+ /** One row of {@link AFFORDANCE_GRANTS}. */
104
+ export interface AffordanceGrantRow {
105
+ /** The resolved CRUD-affordance bit it also needs, or `null` for none. */
106
+ readonly crud: 'create' | 'import' | 'edit' | 'delete' | null;
107
+ /** The caller's object grant it exercises. */
108
+ readonly grant: AffordanceGrant;
109
+ /** For an affordance that offers fields to write: the field-level question. */
110
+ readonly field?: FieldAffordanceGrant;
111
+ }
112
+ /**
113
+ * Every console affordance that offers a write → the grant it reads. Each
114
+ * row's comment names the affordance and the object the grant is asked of;
115
+ * where no object is named, it is the object the surface shows.
116
+ */
117
+ export declare const AFFORDANCE_GRANTS: {
118
+ /** A create form's fields (every `ObjectForm` layout) and its form-wide lock. */
119
+ readonly createFormFields: {
120
+ readonly crud: "create";
121
+ readonly grant: "create";
122
+ readonly field: "create";
123
+ };
124
+ /** An edit form's fields (every `ObjectForm` layout) and its form-wide lock. */
125
+ readonly editFormFields: {
126
+ readonly crud: "edit";
127
+ readonly grant: "update";
128
+ readonly field: "write";
129
+ };
130
+ /**
131
+ * The record page's Edit — the header CTA and the record body's in-place
132
+ * editing (the details body and the highlights strip), each of whose fields
133
+ * asks the update question (objectui#12103) — and the record overlay's
134
+ * inline edit, the same body in the overlay the calendar, kanban and gantt
135
+ * open (`RecordDetailPanel` in `@object-ui/plugin-detail`).
136
+ */
137
+ readonly recordEdit: {
138
+ readonly crud: "edit";
139
+ readonly grant: "update";
140
+ readonly field: "write";
141
+ };
142
+ /** The record page's Delete. */
143
+ readonly recordDelete: {
144
+ readonly crud: "delete";
145
+ readonly grant: "delete";
146
+ };
147
+ /**
148
+ * An object list's New: the console list page's toolbar button and phone
149
+ * "+", and the `object-view` node's toolbar button (`@object-ui/plugin-view`).
150
+ */
151
+ readonly listNew: {
152
+ readonly crud: "create";
153
+ readonly grant: "create";
154
+ };
155
+ /**
156
+ * An object list's Import, and the import wizard's writable target fields —
157
+ * and the AI build panel's spreadsheet import (`ExcelImportBar` in
158
+ * `@object-ui/app-shell`), the wizard's second mount, asked of the object it
159
+ * imports into.
160
+ */
161
+ readonly listImport: {
162
+ readonly crud: "import";
163
+ readonly grant: "create";
164
+ readonly field: "create";
165
+ };
166
+ /** The import wizard's template download — its endpoint answers 403 without the create grant. */
167
+ readonly importTemplate: {
168
+ readonly crud: null;
169
+ readonly grant: "create";
170
+ };
171
+ /** A list's inline (in-cell) editing, each of whose cells asks the update question (objectui#12103). */
172
+ readonly listInlineEdit: {
173
+ readonly crud: "edit";
174
+ readonly grant: "update";
175
+ readonly field: "write";
176
+ };
177
+ /** A list's bulk Delete. */
178
+ readonly listBulkDelete: {
179
+ readonly crud: "delete";
180
+ readonly grant: "delete";
181
+ };
182
+ /**
183
+ * A grid row's Edit — and the writes that update the same rows in place: a
184
+ * grid's bulk field update (a `bulkActionDefs` `update` operation, with its
185
+ * undo and retry), and a gantt task's in-chart edits (a bar's drag, resize
186
+ * and progress, its title edited in place, and its dependency links).
187
+ */
188
+ readonly rowEdit: {
189
+ readonly crud: "edit";
190
+ readonly grant: "update";
191
+ };
192
+ /** A grid row's Delete — and a gantt row's Delete. */
193
+ readonly rowDelete: {
194
+ readonly crud: "delete";
195
+ readonly grant: "delete";
196
+ };
197
+ /** A grid's inline add-record row. */
198
+ readonly gridAddRow: {
199
+ readonly crud: "create";
200
+ readonly grant: "create";
201
+ };
202
+ /**
203
+ * A related list's "+ New", asked of the CHILD object — and a line-items
204
+ * panel's or a master-detail form's add-a-line (its Add, entry row and
205
+ * Duplicate), which creates a child under the same parent.
206
+ */
207
+ readonly relatedNew: {
208
+ readonly crud: "create";
209
+ readonly grant: "create";
210
+ };
211
+ /** A related list row's Edit, asked of the child object. */
212
+ readonly relatedRowEdit: {
213
+ readonly crud: "edit";
214
+ readonly grant: "update";
215
+ };
216
+ /**
217
+ * A related list row's Delete, asked of the child object — and a line-items
218
+ * panel's or a master-detail EDIT form's remove-a-line, which deletes that
219
+ * child on Save.
220
+ */
221
+ readonly relatedRowDelete: {
222
+ readonly crud: "delete";
223
+ readonly grant: "delete";
224
+ };
225
+ /**
226
+ * Setup's Clone of a declared item (objectui#12982): a declared sharing
227
+ * rule's Clone creates an organization rule, asked of the object the
228
+ * environment authors such items as (`sys_sharing_rule`).
229
+ */
230
+ readonly declarationClone: {
231
+ readonly crud: "create";
232
+ readonly grant: "create";
233
+ };
234
+ /** A lookup picker's "Create new", asked of the TARGET object. */
235
+ readonly lookupCreateNew: {
236
+ readonly crud: "create";
237
+ readonly grant: "create";
238
+ };
239
+ /** The record Attachments panel's Upload, asked of `sys_attachment` (storage route, not the data door). */
240
+ readonly attachmentUpload: {
241
+ readonly crud: null;
242
+ readonly grant: "create";
243
+ };
244
+ /** The record Attachments panel's per-row delete, asked of `sys_attachment`. */
245
+ readonly attachmentDelete: {
246
+ readonly crud: null;
247
+ readonly grant: "delete";
248
+ };
249
+ /**
250
+ * The record feed's comment composer and its reply composer, asked of
251
+ * `sys_comment`: each creates a comment row.
252
+ */
253
+ readonly feedComment: {
254
+ readonly crud: "create";
255
+ readonly grant: "create";
256
+ };
257
+ /**
258
+ * The record feed's reaction control, adding the caller's reaction (its Add
259
+ * button, or an emoji the caller has not reacted with), asked of
260
+ * `sys_comment_reaction`: it creates the caller's own reaction row.
261
+ */
262
+ readonly feedReactionAdd: {
263
+ readonly crud: "create";
264
+ readonly grant: "create";
265
+ };
266
+ /**
267
+ * The same control taking the caller's reaction back, asked of
268
+ * `sys_comment_reaction`: it deletes the caller's own reaction row.
269
+ */
270
+ readonly feedReactionRemove: {
271
+ readonly crud: "delete";
272
+ readonly grant: "delete";
273
+ };
274
+ /**
275
+ * The same control on a deployment with no `sys_comment_reaction` (the
276
+ * reactions column store), asked of `sys_comment`: a click updates the
277
+ * comment's `reactions` column.
278
+ */
279
+ readonly feedReactionColumn: {
280
+ readonly crud: "edit";
281
+ readonly grant: "update";
282
+ };
283
+ /**
284
+ * The record feed's Edit on a comment or reply the caller wrote, asked of
285
+ * `sys_comment`: it updates that comment row (objectui#12268). Whose comment
286
+ * it is decides only which rows offer it; the server's own rule decides the
287
+ * write.
288
+ */
289
+ readonly feedCommentEdit: {
290
+ readonly crud: "edit";
291
+ readonly grant: "update";
292
+ };
293
+ /** The same rows' Delete, asked of `sys_comment`: it deletes that comment row. */
294
+ readonly feedCommentDelete: {
295
+ readonly crud: "delete";
296
+ readonly grant: "delete";
297
+ };
298
+ /** A calendar's quick-create: an empty-day click, or a time-range drag in the week / day grid. */
299
+ readonly calendarQuickCreate: {
300
+ readonly crud: "create";
301
+ readonly grant: "create";
302
+ };
303
+ /** A calendar's drag-to-reschedule: moving or resizing an event writes its date fields. */
304
+ readonly calendarReschedule: {
305
+ readonly crud: "edit";
306
+ readonly grant: "update";
307
+ };
308
+ /** A kanban's card move: a cross-column drop writes the record's `groupBy` field. */
309
+ readonly kanbanCardMove: {
310
+ readonly crud: "edit";
311
+ readonly grant: "update";
312
+ };
313
+ };
314
+ /** A console affordance with a row in {@link AFFORDANCE_GRANTS}. */
315
+ export type ConsoleAffordance = keyof typeof AFFORDANCE_GRANTS;
316
+ /** The affordances that offer fields to write. */
317
+ export type FieldAffordance = {
318
+ [K in ConsoleAffordance]: (typeof AFFORDANCE_GRANTS)[K] extends {
319
+ field: FieldAffordanceGrant;
320
+ } ? K : never;
321
+ }[ConsoleAffordance];
322
+ /**
323
+ * The principal surface {@link resolveAffordance} reads — structurally the
324
+ * subset of `usePermissions()` that answers it, declared here so this module
325
+ * stays React-free and binds to no provider.
326
+ */
327
+ export interface AffordanceGrantPrincipal {
328
+ /** `usePermissions().can` — `true` for every grant with no provider mounted. */
329
+ can(object: string, grant: AffordanceGrant): boolean;
330
+ /** The server's effective API operation set for an object; `undefined` leaves the policy as it is. */
331
+ getObjectApiOperations?(object: string): readonly string[] | undefined;
332
+ }
333
+ /** The principal surface {@link resolveFieldAffordance} reads. */
334
+ export interface FieldAffordancePrincipal {
335
+ /** `false` with no provider mounted: no field question is asked. */
336
+ isLoaded: boolean;
337
+ checkField(object: string, field: string, action: FieldAffordanceGrant): boolean;
338
+ }
339
+ /** What an affordance is resolved against. */
340
+ export interface AffordanceSource {
341
+ /** The schema of the object the grant is asked of; `null` reads as the default bucket. */
342
+ objectSchema?: SchemaLike | null;
343
+ /** That object's name. Absent: no grant and no operation set can be asked, so both read open. */
344
+ objectName?: string | null;
345
+ /** The caller's permissions; absent reads open, as no provider does. */
346
+ perms?: AffordanceGrantPrincipal | null;
347
+ }
348
+ /** One affordance's verdict. */
349
+ export interface AffordanceVerdict {
350
+ /** Policy ∧ effective API operation set ∧ the caller's grant. */
351
+ readonly allowed: boolean;
352
+ /** The row's `userActions` predicate envelope — only when {@link allowed}. */
353
+ readonly predicates?: RowCrudPredicates;
354
+ }
355
+ /**
356
+ * Resolve one affordance: its row's CRUD-affordance bit (managed-object policy
357
+ * ∧ the effective API operation set) AND the caller's grant.
358
+ */
359
+ export declare function resolveAffordance(affordance: ConsoleAffordance, { objectSchema, objectName, perms }?: AffordanceSource): AffordanceVerdict;
360
+ /**
361
+ * The field-level half of a field affordance: may the caller write `field`
362
+ * through it? Asks the resolver the row's question (`create` or `write`); with
363
+ * no provider mounted (`isLoaded` false) it asks nothing and answers `true`.
364
+ */
365
+ export declare function resolveFieldAffordance(affordance: FieldAffordance, perms: FieldAffordancePrincipal | null | undefined, objectName: string, field: string): boolean;
366
+ /**
367
+ * The form row for a form's mode: a create form's fields ask the create
368
+ * question, every other form that writes asks the edit question, and a `view`
369
+ * form writes nothing (`undefined`).
370
+ */
371
+ export declare function formFieldsAffordance(mode: string | undefined): 'createFormFields' | 'editFormFields' | undefined;
@@ -0,0 +1,252 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ /**
9
+ * The affordance-to-grant map (objectui#12082): ONE table naming, for every
10
+ * console affordance that offers a write, the grant the caller must hold for
11
+ * it — and ONE resolver every such affordance reads.
12
+ *
13
+ * ## The family this closes
14
+ *
15
+ * Each console surface used to decide on its own which grant its affordance
16
+ * reads, and each surface that decided wrongly was found and fixed alone
17
+ * (objectui#4296, #10107, #11000, #12047). objectui#12082 is the member that
18
+ * hurt most: a create form gated its fields on the EDIT grant, so a
19
+ * create-only role could not fill the form it was allowed to submit. The same
20
+ * pass found the record header not reading the update grant at all, and a
21
+ * lookup offering "Create new" with no grant read either (objectui#12081). The
22
+ * cause is one shape — an affordance spelling its own permission logic — so
23
+ * the fix is one table every affordance reads, not another one-off repair.
24
+ *
25
+ * ## What a row says
26
+ *
27
+ * `crud` — the resolved CRUD-affordance bit the affordance also needs: the
28
+ * object's managed-object policy (ADR-0103, the spec's
29
+ * `resolveCrudAffordances`) intersected with the server's effective API
30
+ * operation set (`/me/permissions` `apiOperations`, objectstack#3391), both
31
+ * through {@link resolveEffectiveCrudAffordances}. `null` for an affordance
32
+ * whose write does not go through the object's generic data door (an
33
+ * attachment upload goes through the storage route; the import template has
34
+ * its own endpoint), so that door's operation set says nothing about it.
35
+ *
36
+ * `grant` — the caller's object grant it exercises, asked as `can(object,
37
+ * grant)` (`MePermissionsProvider` maps `create` → `allowCreate`, `update` →
38
+ * `allowEdit`, `delete` → `allowDelete`).
39
+ *
40
+ * `field` — for an affordance that offers FIELDS to write, the field-level
41
+ * question asked of each one: `create` for an insert, `write` for an update.
42
+ * They differ only for a field the permission set does not mention, which the
43
+ * server's field step lets through so that object admission decides: on an
44
+ * insert that is `allowCreate`, on an update `allowEdit`. A field the set marks
45
+ * `editable: false` is refused on both, and the resolver answers that the same
46
+ * way for both questions. So a create form follows what the server enforces on
47
+ * insert, and adds no rule the server does not have.
48
+ *
49
+ * The `write` question is the server's whole update rule for the field, so it
50
+ * also carries the one field whose update needs an OBJECT grant beyond
51
+ * `allowEdit`: the record's owner, the field the spec's `SystemFieldName`
52
+ * constant names `OWNER_ID`. The server refuses an update that writes it without
53
+ * the transfer grant (`allowTransfer`, or `modifyAllRecords`), so the resolver
54
+ * answers `write` for it with that grant too (objectui#12103). Every row that
55
+ * asks `write` therefore offers the owner field only to a caller who may
56
+ * transfer the record — the in-place edit, the list's in-cell edit and an edit
57
+ * form alike, with no row of its own to forget.
58
+ *
59
+ * ## The verdict
60
+ *
61
+ * An affordance shows when the managed-object policy, the effective API
62
+ * operation set AND the caller's grant all allow it ({@link resolveAffordance}).
63
+ * Its `userActions` predicates (the #2614 object form) are surfaced only then:
64
+ * a predicate narrows, and never re-opens what policy, operation set or grant
65
+ * closed.
66
+ *
67
+ * ## Fail-open, as before
68
+ *
69
+ * With no permission provider mounted, `usePermissions()` answers `can` with
70
+ * `true` and `isLoaded` with `false`, so every grant here reads open and the
71
+ * field question is not asked at all — a standalone embed, a designer preview
72
+ * and a public surface behave as they did before permissions existed, and the
73
+ * server still enforces (the `fieldWriteGate.ts` contract in
74
+ * `@object-ui/plugin-form`). The policy half is not a per-principal answer and
75
+ * does not fail open with it, exactly as `resolveEffectiveCrudAffordances`
76
+ * never did.
77
+ *
78
+ * ## Adding an affordance
79
+ *
80
+ * Add its row here and read it through {@link resolveAffordance} (or
81
+ * {@link resolveFieldAffordance} for fields). The enumeration pin in
82
+ * `@object-ui/plugin-form` (`affordanceGrantMap-12082.test.tsx`) runs every row
83
+ * against four grant shapes and holds the row set to its own expectation table,
84
+ * and its census refuses a console source file that reads a CRUD grant without
85
+ * this map.
86
+ *
87
+ * An affordance that reads NO grant is invisible to a census of grant reads —
88
+ * the calendar's quick-create and drag-to-reschedule and the kanban card move
89
+ * were such affordances until they gained their rows. So the same pin also
90
+ * counts the WRITES: every call site that invokes a create, update or delete on
91
+ * a data source has an entry there naming the row it sits behind and the file
92
+ * that reads it, or the reason it has none. A write site added without an entry
93
+ * turns that pin red.
94
+ */
95
+ import { resolveEffectiveCrudAffordances, } from './managedBy.js';
96
+ /**
97
+ * Every console affordance that offers a write → the grant it reads. Each
98
+ * row's comment names the affordance and the object the grant is asked of;
99
+ * where no object is named, it is the object the surface shows.
100
+ */
101
+ export const AFFORDANCE_GRANTS = {
102
+ /** A create form's fields (every `ObjectForm` layout) and its form-wide lock. */
103
+ createFormFields: { crud: 'create', grant: 'create', field: 'create' },
104
+ /** An edit form's fields (every `ObjectForm` layout) and its form-wide lock. */
105
+ editFormFields: { crud: 'edit', grant: 'update', field: 'write' },
106
+ /**
107
+ * The record page's Edit — the header CTA and the record body's in-place
108
+ * editing (the details body and the highlights strip), each of whose fields
109
+ * asks the update question (objectui#12103) — and the record overlay's
110
+ * inline edit, the same body in the overlay the calendar, kanban and gantt
111
+ * open (`RecordDetailPanel` in `@object-ui/plugin-detail`).
112
+ */
113
+ recordEdit: { crud: 'edit', grant: 'update', field: 'write' },
114
+ /** The record page's Delete. */
115
+ recordDelete: { crud: 'delete', grant: 'delete' },
116
+ /**
117
+ * An object list's New: the console list page's toolbar button and phone
118
+ * "+", and the `object-view` node's toolbar button (`@object-ui/plugin-view`).
119
+ */
120
+ listNew: { crud: 'create', grant: 'create' },
121
+ /**
122
+ * An object list's Import, and the import wizard's writable target fields —
123
+ * and the AI build panel's spreadsheet import (`ExcelImportBar` in
124
+ * `@object-ui/app-shell`), the wizard's second mount, asked of the object it
125
+ * imports into.
126
+ */
127
+ listImport: { crud: 'import', grant: 'create', field: 'create' },
128
+ /** The import wizard's template download — its endpoint answers 403 without the create grant. */
129
+ importTemplate: { crud: null, grant: 'create' },
130
+ /** A list's inline (in-cell) editing, each of whose cells asks the update question (objectui#12103). */
131
+ listInlineEdit: { crud: 'edit', grant: 'update', field: 'write' },
132
+ /** A list's bulk Delete. */
133
+ listBulkDelete: { crud: 'delete', grant: 'delete' },
134
+ /**
135
+ * A grid row's Edit — and the writes that update the same rows in place: a
136
+ * grid's bulk field update (a `bulkActionDefs` `update` operation, with its
137
+ * undo and retry), and a gantt task's in-chart edits (a bar's drag, resize
138
+ * and progress, its title edited in place, and its dependency links).
139
+ */
140
+ rowEdit: { crud: 'edit', grant: 'update' },
141
+ /** A grid row's Delete — and a gantt row's Delete. */
142
+ rowDelete: { crud: 'delete', grant: 'delete' },
143
+ /** A grid's inline add-record row. */
144
+ gridAddRow: { crud: 'create', grant: 'create' },
145
+ /**
146
+ * A related list's "+ New", asked of the CHILD object — and a line-items
147
+ * panel's or a master-detail form's add-a-line (its Add, entry row and
148
+ * Duplicate), which creates a child under the same parent.
149
+ */
150
+ relatedNew: { crud: 'create', grant: 'create' },
151
+ /** A related list row's Edit, asked of the child object. */
152
+ relatedRowEdit: { crud: 'edit', grant: 'update' },
153
+ /**
154
+ * A related list row's Delete, asked of the child object — and a line-items
155
+ * panel's or a master-detail EDIT form's remove-a-line, which deletes that
156
+ * child on Save.
157
+ */
158
+ relatedRowDelete: { crud: 'delete', grant: 'delete' },
159
+ /**
160
+ * Setup's Clone of a declared item (objectui#12982): a declared sharing
161
+ * rule's Clone creates an organization rule, asked of the object the
162
+ * environment authors such items as (`sys_sharing_rule`).
163
+ */
164
+ declarationClone: { crud: 'create', grant: 'create' },
165
+ /** A lookup picker's "Create new", asked of the TARGET object. */
166
+ lookupCreateNew: { crud: 'create', grant: 'create' },
167
+ /** The record Attachments panel's Upload, asked of `sys_attachment` (storage route, not the data door). */
168
+ attachmentUpload: { crud: null, grant: 'create' },
169
+ /** The record Attachments panel's per-row delete, asked of `sys_attachment`. */
170
+ attachmentDelete: { crud: null, grant: 'delete' },
171
+ /**
172
+ * The record feed's comment composer and its reply composer, asked of
173
+ * `sys_comment`: each creates a comment row.
174
+ */
175
+ feedComment: { crud: 'create', grant: 'create' },
176
+ /**
177
+ * The record feed's reaction control, adding the caller's reaction (its Add
178
+ * button, or an emoji the caller has not reacted with), asked of
179
+ * `sys_comment_reaction`: it creates the caller's own reaction row.
180
+ */
181
+ feedReactionAdd: { crud: 'create', grant: 'create' },
182
+ /**
183
+ * The same control taking the caller's reaction back, asked of
184
+ * `sys_comment_reaction`: it deletes the caller's own reaction row.
185
+ */
186
+ feedReactionRemove: { crud: 'delete', grant: 'delete' },
187
+ /**
188
+ * The same control on a deployment with no `sys_comment_reaction` (the
189
+ * reactions column store), asked of `sys_comment`: a click updates the
190
+ * comment's `reactions` column.
191
+ */
192
+ feedReactionColumn: { crud: 'edit', grant: 'update' },
193
+ /**
194
+ * The record feed's Edit on a comment or reply the caller wrote, asked of
195
+ * `sys_comment`: it updates that comment row (objectui#12268). Whose comment
196
+ * it is decides only which rows offer it; the server's own rule decides the
197
+ * write.
198
+ */
199
+ feedCommentEdit: { crud: 'edit', grant: 'update' },
200
+ /** The same rows' Delete, asked of `sys_comment`: it deletes that comment row. */
201
+ feedCommentDelete: { crud: 'delete', grant: 'delete' },
202
+ /** A calendar's quick-create: an empty-day click, or a time-range drag in the week / day grid. */
203
+ calendarQuickCreate: { crud: 'create', grant: 'create' },
204
+ /** A calendar's drag-to-reschedule: moving or resizing an event writes its date fields. */
205
+ calendarReschedule: { crud: 'edit', grant: 'update' },
206
+ /** A kanban's card move: a cross-column drop writes the record's `groupBy` field. */
207
+ kanbanCardMove: { crud: 'edit', grant: 'update' },
208
+ };
209
+ const PREDICATES = {
210
+ create: 'createPredicates',
211
+ import: 'importPredicates',
212
+ edit: 'editPredicates',
213
+ delete: 'deletePredicates',
214
+ };
215
+ /**
216
+ * Resolve one affordance: its row's CRUD-affordance bit (managed-object policy
217
+ * ∧ the effective API operation set) AND the caller's grant.
218
+ */
219
+ export function resolveAffordance(affordance, { objectSchema, objectName, perms } = {}) {
220
+ const row = AFFORDANCE_GRANTS[affordance];
221
+ let predicates;
222
+ if (row.crud !== null) {
223
+ const ops = objectName ? perms?.getObjectApiOperations?.(objectName) : undefined;
224
+ const crud = resolveEffectiveCrudAffordances(objectSchema, ops);
225
+ if (!crud[row.crud])
226
+ return { allowed: false };
227
+ predicates = crud[PREDICATES[row.crud]];
228
+ }
229
+ if (objectName && perms && !perms.can(objectName, row.grant))
230
+ return { allowed: false };
231
+ return predicates ? { allowed: true, predicates } : { allowed: true };
232
+ }
233
+ /**
234
+ * The field-level half of a field affordance: may the caller write `field`
235
+ * through it? Asks the resolver the row's question (`create` or `write`); with
236
+ * no provider mounted (`isLoaded` false) it asks nothing and answers `true`.
237
+ */
238
+ export function resolveFieldAffordance(affordance, perms, objectName, field) {
239
+ if (!perms?.isLoaded)
240
+ return true;
241
+ return perms.checkField(objectName, field, AFFORDANCE_GRANTS[affordance].field);
242
+ }
243
+ /**
244
+ * The form row for a form's mode: a create form's fields ask the create
245
+ * question, every other form that writes asks the edit question, and a `view`
246
+ * form writes nothing (`undefined`).
247
+ */
248
+ export function formFieldsAffordance(mode) {
249
+ if (mode === 'view')
250
+ return undefined;
251
+ return mode === 'create' ? 'createFormFields' : 'editFormFields';
252
+ }
@@ -9,6 +9,19 @@ export interface ChartResultField {
9
9
  name: string;
10
10
  label?: string;
11
11
  format?: string;
12
+ /**
13
+ * The aggregate the measure column carries — the spec's
14
+ * `AnalyticsResult.fields[].aggregate`, which the dataset answer states on
15
+ * every measure column whose measure declares one, labelled or not
16
+ * (objectstack#21995). Absent on dimension columns and derived measures.
17
+ *
18
+ * Typed `string`, not the closed vocabulary, for the reason
19
+ * {@link BuiltinAggregateCarrier} gives: a JSON response is handed here
20
+ * as-is, so the value is judged at the read ({@link isIntegerAggregate}).
21
+ * {@link buildChartSeries} copies it onto each series it derives from the
22
+ * column, so a value axis can tell a count from a sum (objectui#11681).
23
+ */
24
+ aggregate?: string;
12
25
  }
13
26
  /**
14
27
  * The OPTIONAL wire discriminator naming which server-side BUILT-IN default
@@ -56,6 +69,13 @@ export type ChartMeasureField = ChartResultField & BuiltinAggregateCarrier;
56
69
  export interface ChartSeriesBinding {
57
70
  dataKey: string;
58
71
  label: string;
72
+ /**
73
+ * The aggregate of the measure this series plots, copied from its result
74
+ * column's `aggregate` (objectui#11681). Renderer-internal: the chart's value
75
+ * axis draws whole-number ticks when every series on it plots an
76
+ * {@link isIntegerAggregate} measure. Absent when the column states none.
77
+ */
78
+ aggregate?: string;
59
79
  }
60
80
  export interface ChartSeriesResult {
61
81
  data: Array<Record<string, unknown>>;
@@ -73,6 +93,20 @@ export interface ChartSeriesResult {
73
93
  export declare const BUILTIN_AGGREGATES: readonly ["count", "sum", "avg", "min", "max", "count_distinct"];
74
94
  /** One member of {@link BUILTIN_AGGREGATES}. */
75
95
  export type BuiltinAggregate = (typeof BUILTIN_AGGREGATES)[number];
96
+ /**
97
+ * The aggregates whose every value is a whole number: a count of rows, or of
98
+ * distinct values (objectui#11681, triage ruling Q3 A on that card). `sum` /
99
+ * `min` / `max` over an integer field would qualify too, but the answer does
100
+ * not state the source field's scale, so they are not judged integer here.
101
+ */
102
+ export declare const INTEGER_AGGREGATES: readonly ["count", "count_distinct"];
103
+ /**
104
+ * Whether a measure column's `aggregate` makes every value a whole number — a
105
+ * value axis that plots only such measures takes integer ticks
106
+ * (objectui#11681). Judged over an untrusted wire value; anything else,
107
+ * absence included, answers `false`, so the axis keeps its default.
108
+ */
109
+ export declare function isIntegerAggregate(value: unknown): boolean;
76
110
  /** Runtime membership test for {@link BUILTIN_AGGREGATES}, over an untrusted wire value. */
77
111
  export declare function isBuiltinAggregate(value: unknown): value is BuiltinAggregate;
78
112
  /**