@adminiumjs/adminium 0.3.29 → 0.3.30

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 (104) hide show
  1. package/dist/assistant/actions.js +13 -9
  2. package/dist/assistant/actions.js.map +1 -1
  3. package/dist/automations/actions/record-write.d.ts.map +1 -1
  4. package/dist/automations/actions/record-write.js +18 -2
  5. package/dist/automations/actions/record-write.js.map +1 -1
  6. package/dist/compose.d.ts.map +1 -1
  7. package/dist/compose.js +42 -9
  8. package/dist/compose.js.map +1 -1
  9. package/dist/connections/column-rules-validation.d.ts.map +1 -1
  10. package/dist/connections/column-rules-validation.js +4 -0
  11. package/dist/connections/column-rules-validation.js.map +1 -1
  12. package/dist/connections/effective-schema.d.ts +4 -0
  13. package/dist/connections/effective-schema.d.ts.map +1 -1
  14. package/dist/connections/effective-schema.js +1 -0
  15. package/dist/connections/effective-schema.js.map +1 -1
  16. package/dist/crud/row-writes.d.ts.map +1 -1
  17. package/dist/crud/row-writes.js +30 -2
  18. package/dist/crud/row-writes.js.map +1 -1
  19. package/dist/crud/states.d.ts +2 -0
  20. package/dist/crud/states.d.ts.map +1 -1
  21. package/dist/crud/states.js +42 -2
  22. package/dist/crud/states.js.map +1 -1
  23. package/dist/crud/unbuilt-rules.d.ts.map +1 -1
  24. package/dist/crud/unbuilt-rules.js +2 -0
  25. package/dist/crud/unbuilt-rules.js.map +1 -1
  26. package/dist/crud/write-service.d.ts.map +1 -1
  27. package/dist/crud/write-service.js +64 -4
  28. package/dist/crud/write-service.js.map +1 -1
  29. package/dist/crud/write-tree.d.ts +3 -0
  30. package/dist/crud/write-tree.d.ts.map +1 -1
  31. package/dist/crud/write-tree.js.map +1 -1
  32. package/dist/email/file-reach.d.ts +16 -6
  33. package/dist/email/file-reach.d.ts.map +1 -1
  34. package/dist/email/file-reach.js +17 -8
  35. package/dist/email/file-reach.js.map +1 -1
  36. package/dist/files/named-uploads.d.ts +35 -0
  37. package/dist/files/named-uploads.d.ts.map +1 -0
  38. package/dist/files/named-uploads.js +72 -0
  39. package/dist/files/named-uploads.js.map +1 -0
  40. package/dist/files/reconcile.d.ts +2 -0
  41. package/dist/files/reconcile.d.ts.map +1 -1
  42. package/dist/files/reconcile.js +15 -5
  43. package/dist/files/reconcile.js.map +1 -1
  44. package/dist/project/code/db.d.ts.map +1 -1
  45. package/dist/project/code/db.js +4 -2
  46. package/dist/project/code/db.js.map +1 -1
  47. package/dist/routes/data/index.d.ts.map +1 -1
  48. package/dist/routes/data/index.js +90 -11
  49. package/dist/routes/data/index.js.map +1 -1
  50. package/dist/routes/data/refusal-scrub.d.ts +8 -0
  51. package/dist/routes/data/refusal-scrub.d.ts.map +1 -1
  52. package/dist/routes/data/refusal-scrub.js +14 -0
  53. package/dist/routes/data/refusal-scrub.js.map +1 -1
  54. package/dist/routes/email-templates/index.d.ts.map +1 -1
  55. package/dist/routes/email-templates/index.js +5 -1
  56. package/dist/routes/email-templates/index.js.map +1 -1
  57. package/dist/routes/files/index.d.ts.map +1 -1
  58. package/dist/routes/files/index.js +5 -2
  59. package/dist/routes/files/index.js.map +1 -1
  60. package/dist/routes/public/index.d.ts.map +1 -1
  61. package/dist/routes/public/index.js +21 -5
  62. package/dist/routes/public/index.js.map +1 -1
  63. package/node_modules/@adminium/adapter-mysql/package.json +1 -1
  64. package/node_modules/@adminium/adapter-postgres/package.json +1 -1
  65. package/node_modules/@adminium/adapter-sqlite/package.json +1 -1
  66. package/node_modules/@adminium/add-on-contracts/package.json +1 -1
  67. package/node_modules/@adminium/engine/package.json +1 -1
  68. package/node_modules/@adminium/i18n/package.json +1 -1
  69. package/node_modules/@adminium/llm/package.json +1 -1
  70. package/node_modules/@adminium/manifest/dist/roles.d.ts +3 -0
  71. package/node_modules/@adminium/manifest/dist/roles.d.ts.map +1 -1
  72. package/node_modules/@adminium/manifest/dist/roles.js +5 -0
  73. package/node_modules/@adminium/manifest/dist/roles.js.map +1 -1
  74. package/node_modules/@adminium/manifest/dist/schema.d.ts +8 -0
  75. package/node_modules/@adminium/manifest/dist/schema.d.ts.map +1 -1
  76. package/node_modules/@adminium/manifest/dist/schema.js +1 -0
  77. package/node_modules/@adminium/manifest/dist/schema.js.map +1 -1
  78. package/node_modules/@adminium/manifest/dist/shapes.js +4 -0
  79. package/node_modules/@adminium/manifest/dist/shapes.js.map +1 -1
  80. package/node_modules/@adminium/manifest/dist/states.d.ts +8 -1
  81. package/node_modules/@adminium/manifest/dist/states.d.ts.map +1 -1
  82. package/node_modules/@adminium/manifest/dist/states.js +40 -3
  83. package/node_modules/@adminium/manifest/dist/states.js.map +1 -1
  84. package/node_modules/@adminium/manifest/dist/words.d.ts +1 -1
  85. package/node_modules/@adminium/manifest/dist/words.d.ts.map +1 -1
  86. package/node_modules/@adminium/manifest/dist/words.js +7 -0
  87. package/node_modules/@adminium/manifest/dist/words.js.map +1 -1
  88. package/node_modules/@adminium/manifest/package.json +1 -1
  89. package/node_modules/@adminium/meta/dist/repos/files.d.ts +23 -3
  90. package/node_modules/@adminium/meta/dist/repos/files.d.ts.map +1 -1
  91. package/node_modules/@adminium/meta/dist/repos/files.js +63 -15
  92. package/node_modules/@adminium/meta/dist/repos/files.js.map +1 -1
  93. package/node_modules/@adminium/meta/dist/schema/json-payloads.d.ts +1 -0
  94. package/node_modules/@adminium/meta/dist/schema/json-payloads.d.ts.map +1 -1
  95. package/node_modules/@adminium/meta/dist/schema/json-payloads.js +2 -0
  96. package/node_modules/@adminium/meta/dist/schema/json-payloads.js.map +1 -1
  97. package/node_modules/@adminium/meta/package.json +1 -1
  98. package/node_modules/@adminium/schema-import/package.json +1 -1
  99. package/node_modules/@adminium/widgets/package.json +1 -1
  100. package/package.json +12 -12
  101. package/skills/adminium-app/references/manifest/INDEX.md +3 -3
  102. package/skills/adminium-app/references/manifest/requiredschema--states-3.md +15 -7
  103. package/skills/adminium-app/references/manifest/requiredschema--states-4.md +43 -0
  104. package/skills/adminium-app/references/manifest/requiredschema--states.md +1 -0
@@ -63,7 +63,8 @@ import { recipientLocale } from '../../i18n/server-i18n.js';
63
63
  import { TreeCheckRefused } from '../../public-api/tree-checks.js';
64
64
  import { staffTreeRules } from './tree.js';
65
65
  import { declaredColumns, expectColumnOf, priceCheck, staffRetryKey } from './staff-quote.js';
66
- import { scrubRefusal } from './refusal-scrub.js';
66
+ import { scrubRefusal, scrubUnreadRefusal } from './refusal-scrub.js';
67
+ import { refusalTableOf } from '../../crud/refusal-table.js';
67
68
  import { clientKeySecret } from '../public/tree.js';
68
69
  /**
69
70
  * How many links one record's field reads and replaces.
@@ -301,6 +302,24 @@ export function dataRoutes(deps) {
301
302
  Object.defineProperty(error, 'message', { value: scrubbed.message, configurable: true, writable: true });
302
303
  }
303
304
  });
305
+ /*
306
+ * A refusal about another row than the one written (an effect's row, the
307
+ * parent a new child moves) told to somebody who may not read that row's
308
+ * table: it names nothing of the row — its state, what its move waits for.
309
+ */
310
+ app.addHook('onError', async (request, _reply, error) => {
311
+ if (!(error instanceof AppError))
312
+ return;
313
+ const about = refusalTableOf(error);
314
+ const connectionId = request.params?.connectionId;
315
+ if (about === undefined || typeof connectionId !== 'string')
316
+ return;
317
+ if (await request.can(`table:${connectionId}:${about}:read`))
318
+ return;
319
+ const scrubbed = scrubUnreadRefusal(error);
320
+ Object.defineProperty(error, 'details', { value: scrubbed.details, configurable: true });
321
+ Object.defineProperty(error, 'message', { value: scrubbed.message, configurable: true, writable: true });
322
+ });
304
323
  /*
305
324
  * A REQUEST OF SOMEBODY WHO REACHES ONLY SOME ROWS (`crud/row-limit.ts`).
306
325
  *
@@ -817,6 +836,8 @@ export function dataRoutes(deps) {
817
836
  })();
818
837
  undo.added.push(keyOf(written));
819
838
  (undo.addedRows ??= []).push(written);
839
+ // The parent outside this record a new child moved (`moveOnCreate`).
840
+ options.effects?.push(...effectsOf(prepared.values));
820
841
  settled.push({ action: 'create', record: written, before: null });
821
842
  options.events?.push({ table: child.child, action: 'create', pk: keyOf(written), row: written });
822
843
  }
@@ -1544,6 +1565,28 @@ export function dataRoutes(deps) {
1544
1565
  if (entry.fileIds.length > 0 && deps.files !== undefined) {
1545
1566
  await deps.files.restoreAll(entry.fileIds);
1546
1567
  }
1568
+ /*
1569
+ * A change taken back puts its files back too: a file of the record's
1570
+ * it names again comes out of the bin the change put it in, and one it
1571
+ * names no more goes there, as a replaced one does. A new row taken
1572
+ * back takes its files with it, as a deleted one does.
1573
+ */
1574
+ if (deps.files !== undefined && entry.action !== 'delete') {
1575
+ for (const row of changed) {
1576
+ const entity = { connectionId: entry.connectionId, table: table.id, pk: row.pk, label: pkLabel(table, row.pk) };
1577
+ if (entry.action === 'create') {
1578
+ if (row.record !== null)
1579
+ await trashRecordFiles(undone, entity, row.record);
1580
+ continue;
1581
+ }
1582
+ if (row.before === null)
1583
+ continue;
1584
+ const after = await fetchByPk(db, table, row.pk);
1585
+ if (after === undefined)
1586
+ continue;
1587
+ await deps.files.reconcile({ connectionId: entry.connectionId, table: table.id, entity, before: row.before, after, ruled: ruledBlocksOf(table.columns.values()), restoring: true });
1588
+ }
1589
+ }
1547
1590
  await app.rbac.audit(request, {
1548
1591
  category: 'data',
1549
1592
  action: 'record.undo',
@@ -1900,10 +1943,12 @@ export function dataRoutes(deps) {
1900
1943
  context,
1901
1944
  recheck: (final) => assertFileColumns(ctx, final),
1902
1945
  mapError: (error) => mapDbError(error, ctx.table),
1903
- announce: async (row, _values, posted) => {
1946
+ announce: async (row, written, posted) => {
1904
1947
  told = postingAnswers(posted);
1905
1948
  await afterMutation(request, ctx, 'create', recordRef(ctx, Object.fromEntries(ctx.table.primaryKey.map((c) => [c, row[c]]))), null, row);
1906
1949
  await tellPostings(app, { connectionId: ctx.connectionId, view: ctx.view, postings: posted, origin: 'dashboard', request });
1950
+ // The parent a new row moved (`moveOnCreate`).
1951
+ await announceEffects(app, { connectionId: ctx.connectionId, view: ctx.view, effects: effectsOf(written), origin: 'bulk', request });
1907
1952
  },
1908
1953
  });
1909
1954
  const key = ctx.table.primaryKey.length === 1 ? inserted[ctx.table.primaryKey[0]] : Object.fromEntries(ctx.table.primaryKey.map((c) => [c, inserted[c]]));
@@ -2118,9 +2163,24 @@ export function dataRoutes(deps) {
2118
2163
  }
2119
2164
  });
2120
2165
  const okCount = results.filter((r) => r.ok).length;
2166
+ /*
2167
+ * Each row's files, as a row's own write moves them: a deleted row's go
2168
+ * to the bin (and come back with its undo), a changed row holds what it
2169
+ * names now and lets go of what it named.
2170
+ */
2171
+ const trashedFileIds = [];
2172
+ if (deps.files !== undefined) {
2173
+ for (const event of events) {
2174
+ const entity = recordRef(ctx, event.pk);
2175
+ if (action === 'delete')
2176
+ trashedFileIds.push(...(await trashRecordFiles(ctx, entity, event.before)));
2177
+ else
2178
+ await deps.files.reconcile({ connectionId: ctx.connectionId, table: ctx.table.id, entity, before: event.before, after: event.after, ruled: ruledBlocksOf(ctx.table.columns.values()) });
2179
+ }
2180
+ }
2121
2181
  const undoToken = okCount === 0
2122
2182
  ? null
2123
- : await issueUndo(request, ctx, action, beforeImages, afterImages, action === 'update' && values !== null ? changedColumns(values, readied) : [], [], [], [], effected);
2183
+ : await issueUndo(request, ctx, action, beforeImages, afterImages, action === 'update' && values !== null ? changedColumns(values, readied) : [], trashedFileIds, [], [], effected);
2124
2184
  await app.rbac.audit(request, {
2125
2185
  category: 'data',
2126
2186
  action: `record.bulk-${action}`,
@@ -2810,13 +2870,17 @@ export function dataRoutes(deps) {
2810
2870
  }
2811
2871
  return written;
2812
2872
  });
2873
+ // The parents the new rows moved (`moveOnCreate`): such a save offers no Undo.
2874
+ const moved = prepared.flatMap((row) => effectsOf(row.values));
2813
2875
  // One token over every row: the create-undo path already deletes
2814
2876
  // each `after` it holds, in order.
2815
- undoToken = await issueUndo(request, ctx, 'create', [], rows);
2877
+ if (moved.length === 0)
2878
+ undoToken = await issueUndo(request, ctx, 'create', [], rows);
2816
2879
  for (const row of rows) {
2817
2880
  const pk = Object.fromEntries(ctx.table.primaryKey.map((c) => [c, row[c]]));
2818
2881
  await afterMutation(request, ctx, 'create', recordRef(ctx, pk), null, row);
2819
2882
  }
2883
+ await announceEffects(app, { connectionId: ctx.connectionId, view: ctx.view, effects: moved, origin: 'dashboard', request });
2820
2884
  await writes.afterEach('create', ctx.target, context, rows.map((record) => ({ record, before: null })));
2821
2885
  // The reply is the row as stored, its own totals settled after the commit.
2822
2886
  const [first] = (await writes.stored(ctx.target, rows.slice(0, 1)));
@@ -2838,10 +2902,12 @@ export function dataRoutes(deps) {
2838
2902
  if (tree.outcome.replayed)
2839
2903
  return reply.status(200).send({ data: staffRow(ctx.dialect, created, ctx.readTable, ctx.unmasked), undoToken: null, replayed: true });
2840
2904
  const told = postingAnswers(tree.outcome.postings);
2841
- // A save that posted is not undone by a token: the rule's own way back is.
2842
- if (told === undefined && !pricedBy(tree.outcome.adjusted))
2905
+ // A save that posted is not undone by a token: the rule's own way back is. Nor one that moved its parent.
2906
+ if (told === undefined && !pricedBy(tree.outcome.adjusted) && tree.outcome.effects === undefined)
2843
2907
  undoToken = await issueUndo(request, ctx, 'create', [], [created], [], [], [], []);
2844
2908
  await tellPostings(app, { connectionId: ctx.connectionId, view: ctx.view, postings: tree.outcome.postings, origin: 'dashboard', request });
2909
+ // The parent the new row moved (`moveOnCreate`): announced as a change of its own, after the row's.
2910
+ await announceEffects(app, { connectionId: ctx.connectionId, view: ctx.view, effects: tree.outcome.effects, origin: 'dashboard', request });
2845
2911
  const once = onceFor(ctx.connectionId, request.user?.id ?? null, tree.outcome.rows.map((made) => ({ table: made.node.target.table, row: made.record })));
2846
2912
  return reply.status(201).send({ data: staffRow(ctx.dialect, created, ctx.readTable, ctx.unmasked), undoToken, ...(told === undefined ? {} : { postings: told }), ...(once === undefined ? {} : { once }), ...(await pricedAs(request, ctx, tree.outcome.adjusted, tree.outcome.rows)), ...(await paidAs(request, ctx, treeRows(tree.outcome), tree.outcome.postings)) });
2847
2913
  }
@@ -2855,16 +2921,19 @@ export function dataRoutes(deps) {
2855
2921
  context,
2856
2922
  recheck: (final) => assertFileColumns(ctx, final),
2857
2923
  mapError: (error) => mapDbError(error, ctx.table),
2858
- announce: async (row, _values, posted, adjusted) => {
2924
+ announce: async (row, written, posted, adjusted) => {
2859
2925
  const pk = Object.fromEntries(ctx.table.primaryKey.map((c) => [c, row[c]]));
2860
2926
  told = postingAnswers(posted);
2861
2927
  asked = adjusted;
2862
2928
  outcomes = posted;
2863
- // A save that moved an order's price has no Undo: its way back is another save, priced in its turn.
2864
- if (told === undefined && !pricedBy(adjusted))
2929
+ // The parent the new row moved (`moveOnCreate`): announced as a change of its own, after the row's.
2930
+ const moved = effectsOf(written);
2931
+ // A save that moved an order's price has no Undo: its way back is another save, priced in its turn. Nor one that moved its parent.
2932
+ if (told === undefined && !pricedBy(adjusted) && moved.length === 0)
2865
2933
  undoToken = await issueUndo(request, ctx, 'create', [], [row]);
2866
2934
  await afterMutation(request, ctx, 'create', recordRef(ctx, pk), null, row);
2867
2935
  await tellPostings(app, { connectionId: ctx.connectionId, view: ctx.view, postings: posted, origin: 'dashboard', request });
2936
+ await announceEffects(app, { connectionId: ctx.connectionId, view: ctx.view, effects: moved, origin: 'dashboard', request });
2868
2937
  },
2869
2938
  });
2870
2939
  const once = onceFor(ctx.connectionId, request.user?.id ?? null, [{ table: ctx.table, row: inserted }]);
@@ -2893,9 +2962,10 @@ export function dataRoutes(deps) {
2893
2962
  return reply.status(200).send({ data: staffRow(ctx.dialect, created, ctx.readTable, ctx.unmasked), undoToken: null, replayed: true });
2894
2963
  const pk = Object.fromEntries(ctx.table.primaryKey.map((c) => [c, created[c]]));
2895
2964
  const told = postingAnswers(tree.outcome.postings);
2896
- if (told === undefined && !pricedBy(tree.outcome.adjusted))
2965
+ if (told === undefined && !pricedBy(tree.outcome.adjusted) && tree.outcome.effects === undefined)
2897
2966
  undoToken = await issueUndo(request, ctx, 'create', [], [created], [], [], tree.links, tree.children);
2898
2967
  await tellPostings(app, { connectionId: ctx.connectionId, view: ctx.view, postings: tree.outcome.postings, origin: 'dashboard', request });
2968
+ await announceEffects(app, { connectionId: ctx.connectionId, view: ctx.view, effects: tree.outcome.effects, origin: 'dashboard', request });
2899
2969
  for (const event of tree.events)
2900
2970
  publishChildWrite(app, { connectionId: ctx.connectionId, ...event });
2901
2971
  await tellChildMentions(request, ctx, tree.events);
@@ -2957,6 +3027,8 @@ export function dataRoutes(deps) {
2957
3027
  const written = [];
2958
3028
  const childWrites = [];
2959
3029
  const childEvents = [];
3030
+ /** The parents the new rows moved (`moveOnCreate`): the record's own, and its children's. */
3031
+ const childEffects = [];
2960
3032
  const numbered = children.map((requested) => ({ target: childTargetOf(ctx, requested.child, ctx.db), row: {} }));
2961
3033
  const agrees = await recordAgrees(ctx);
2962
3034
  let inserted = await writes.transaction(ctx.target, [prepared.values], async (trx) => {
@@ -2980,6 +3052,7 @@ export function dataRoutes(deps) {
2980
3052
  childWrites.push(await applyChildren(ctx, tdb, requested, row[requested.child.parentKeyColumn], context, {
2981
3053
  existing: [],
2982
3054
  events: childEvents,
3055
+ effects: childEffects,
2983
3056
  }));
2984
3057
  }
2985
3058
  // The record as its children left it: a total over them has moved,
@@ -2997,8 +3070,12 @@ export function dataRoutes(deps) {
2997
3070
  return made;
2998
3071
  }, numbered);
2999
3072
  const pk = Object.fromEntries(ctx.table.primaryKey.map((c) => [c, inserted[c]]));
3000
- undoToken = await issueUndo(request, ctx, 'create', [], [inserted], [], [], written, childWrites);
3073
+ const moved = [...effectsOf(prepared.values), ...childEffects];
3074
+ // A save that moved a parent offers no Undo: taking the row back would leave the parent moved.
3075
+ if (moved.length === 0)
3076
+ undoToken = await issueUndo(request, ctx, 'create', [], [inserted], [], [], written, childWrites);
3001
3077
  await afterMutation(request, ctx, 'create', recordRef(ctx, pk), null, inserted);
3078
+ await announceEffects(app, { connectionId: ctx.connectionId, view: ctx.view, effects: moved, origin: 'dashboard', request });
3002
3079
  for (const event of childEvents)
3003
3080
  publishChildWrite(app, { connectionId: ctx.connectionId, ...event });
3004
3081
  await tellChildMentions(request, ctx, childEvents);
@@ -3452,6 +3529,8 @@ export function dataRoutes(deps) {
3452
3529
  context,
3453
3530
  mapError: (error) => mapDbError(error, ctx.table),
3454
3531
  ...withinReach(ctx, 'delete'),
3532
+ // A DELETE that matched nothing (somebody else's landed first) announces nothing: no file to the bin, no Undo.
3533
+ skipIfNone: true,
3455
3534
  announce: async (_count, _before, adjusted) => {
3456
3535
  asked = adjusted;
3457
3536
  const entity = recordRef(ctx, pk);