@stonecrop/nuxt 0.34.0 → 0.36.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.
package/README.md CHANGED
@@ -427,7 +427,7 @@ await dispatchAction({ name: 'plan' }, 'SUBMIT', [recordId])
427
427
  | `setMeta(fn)` | `(fn: (ctx) => Doctype \| Promise<Doctype>) => void` | Sets the `getMeta` function on the Registry. Called by `useStonecrop({ doctype: 'slug' })` to lazy-load that doctype's metadata. `ctx` = `{ path, segments }`. |
428
428
  | `setClient(client)` | `(client: DataClient) => void` | Set the data client for record fetching. Throws if stonecrop not available. |
429
429
  | `getClient()` | `() => DataClient \| undefined` | Get the currently configured client. |
430
- | `dispatchAction(doctype, action, args)` | `Promise<{ success, data, error }>` | Dispatch an action via the configured client. Returns error if doctype not found in registry. |
430
+ | `dispatchAction(doctype, action, args)` | `Promise<{ success, data, error }>` | Dispatch an action via the configured client, storing the result's `record` (the server's read of the record after the action). Returns error if doctype not found in registry. |
431
431
 
432
432
  ## Advanced Features
433
433
 
@@ -570,8 +570,10 @@ The CLI will detect that you're in a Nuxt project and prompt for features:
570
570
 
571
571
  ? Select features to install
572
572
  ◉ @stonecrop/nuxt - Frontend module
573
+ ◯ @stonecrop/graphql-client - GraphQL client
573
574
  ◯ @stonecrop/nuxt-grafserv - GraphQL server
574
575
  ◯ @stonecrop/casl-middleware - Authorization
576
+ ◯ @stonecrop/rockfoil - PostGraphile middleware
575
577
  ◉ Sample doctypes
576
578
  ```
577
579
 
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stonecrop/nuxt",
3
3
  "configKey": "stonecrop",
4
- "version": "0.34.0",
4
+ "version": "0.36.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.3",
7
7
  "unbuild": "unknown"
@@ -174,5 +174,5 @@ function onRemoveRow(row) {
174
174
  </script>
175
175
 
176
176
  <style scoped>
177
- .actions-panel{padding:.5em 1em}.actions-panel :deep(.atable-row>td){border-top:1px solid var(--sc-row-border-color,#e5e7eb);padding:var(--sc-atable-row-padding,.25rem) .75em;vertical-align:middle}.actions-panel :deep(input[type=text]){border:1px solid var(--sc-gray-20,#d1d5db);border-radius:3px;font-family:inherit;font-size:inherit;padding:.25em .5em;width:100%}.actions-panel :deep(input[type=text]:focus){border-color:var(--sc-blue-40,#3b82f6);outline:none}.cell-readonly{color:var(--sc-gray-60,#4b5563);cursor:default}.badge{border-radius:9999px;display:inline-block;font-size:.75rem;font-weight:500;padding:.125em .5em}.badge-transition{background:#dbeafe;color:#1e40af}.badge-self-transition{background:#fef3c7;color:#92400e}.badge-command{background:#f3e8ff;color:#6b21a8}.badge-trigger{background:#dcfce7;color:#166534}.actions-empty{color:#9ca3af;font-style:italic;padding:1rem 0;text-align:center}.panel-footer{display:flex;gap:.5rem;padding:.75rem 0 .25rem}.add-row{background:var(--sc-blue-40,#3b82f6);border:none;border-radius:.4rem;color:#fff;cursor:pointer;font-size:.8125rem;font-weight:500;padding:.4em .9em}.row-detail{gap:.75rem}.detail-field,.row-detail{display:flex;flex-direction:column}.detail-field{gap:.25rem}.detail-label{color:var(--sc-header-text-color,#374151);font-size:.75rem;font-weight:600;letter-spacing:.05em;text-transform:uppercase}.detail-value{color:var(--sc-gray-60,#4b5563);font-size:.8125rem}.client-handler-editor{display:flex;flex-direction:column;gap:.5rem}.handler-label{color:var(--sc-header-text-color,#374151);font-size:.75rem;font-weight:600;letter-spacing:.05em;text-transform:uppercase}.remove-row{align-self:flex-start;background:none;border:1px solid #fca5a5;border-radius:.3rem;color:#b91c1c;cursor:pointer;font-size:.75rem;padding:.3em .75em}
177
+ .actions-panel{padding:.5em 1em}.actions-panel :deep(.atable-row>td){border-top:1px solid var(--sc-row-border-color);padding:var(--sc-atable-row-padding) .75em;vertical-align:middle}.actions-panel :deep(input[type=text]){border:1px solid var(--sc-gray-20);border-radius:var(--sc-border-radius);font-family:inherit;font-size:inherit;padding:.25em .5em;width:100%}.actions-panel :deep(input[type=text]:focus){border-color:var(--sc-input-active-border-color);outline:none}.cell-readonly{color:var(--sc-gray-60);cursor:default}.badge{border-radius:9999px;display:inline-block;font-size:.75rem;font-weight:500;padding:.125em .5em}.badge-transition{background:var(--sc-badge-brand-bg);color:var(--sc-badge-brand-text)}.badge-command,.badge-self-transition,.badge-trigger{background:var(--sc-badge-neutral-bg);color:var(--sc-badge-neutral-text)}.actions-empty{color:var(--sc-header-text-color);font-style:italic;padding:1rem 0;text-align:center}.panel-footer{display:flex;gap:.5rem;padding:.75rem 0 .25rem}.add-row{background:var(--sc-primary-color);border:none;border-radius:var(--sc-border-radius);color:var(--sc-primary-text-color);cursor:pointer;font-size:.8125rem;font-weight:500;padding:.4em .9em}.row-detail{gap:.75rem}.detail-field,.row-detail{display:flex;flex-direction:column}.detail-field{gap:.25rem}.detail-label{color:var(--sc-header-text-color);font-size:.75rem;font-weight:600;letter-spacing:.05em;text-transform:uppercase}.detail-value{color:var(--sc-gray-60);font-size:.8125rem}.client-handler-editor{display:flex;flex-direction:column;gap:.5rem}.handler-label{color:var(--sc-header-text-color);font-size:.75rem;font-weight:600;letter-spacing:.05em;text-transform:uppercase}.remove-row{align-self:flex-start;background:none;border:1px solid var(--sc-badge-danger-accent);border-radius:var(--sc-border-radius);color:var(--sc-badge-danger-text);cursor:pointer;font-size:.75rem;padding:.3em .75em}
178
178
  </style>
@@ -336,5 +336,5 @@ function bool(v) {
336
336
  </script>
337
337
 
338
338
  <style scoped>
339
- .fields-panel{padding:.5em 1em}.fields-panel :deep(.atable-row>td){border-top:1px solid var(--sc-row-border-color,#e5e7eb);padding:var(--sc-atable-row-padding,.125rem) .75em;vertical-align:middle}.fields-panel :deep(input[type=text]),.fields-panel :deep(select){border:1px solid var(--sc-gray-20,#d1d5db);border-radius:3px;font-family:inherit;font-size:inherit;padding:.25em .5em;width:100%}.fields-panel :deep(input.locked),.fields-panel :deep(select.locked){background:var(--sc-gray-10,#f3f4f6);color:#6b7280;cursor:not-allowed}.fields-panel :deep(input.json-invalid){background:#fef2f2;border-color:#f87171}.center{text-align:center}.badge{border-radius:9999px;display:inline-block;font-size:.75rem;font-weight:500;padding:.125em .5em}.badge-manual{background:#dcfce7;color:#166534}.badge-introspected{background:#e0e7ff;color:#3730a3}.field-detail{display:grid;gap:.75rem 1rem;grid-template-columns:repeat(auto-fill,minmax(14rem,1fr));padding:.75rem 1rem}.field-prop{color:var(--sc-header-text-color,#374151);display:flex;flex-direction:column;font-size:.75rem;gap:.25rem}.field-prop-inline{align-items:center;flex-direction:row;gap:.5rem}.field-prop-wide{grid-column:1/-1}.field-detail-actions{align-items:center;display:flex;gap:1rem;justify-content:space-between;padding:0 1rem .75rem}.locked-note{color:#6b7280;font-size:.75rem;font-style:italic}.btn-add{background:none;border:1px dashed var(--sc-gray-20,#d1d5db);border-radius:4px;color:var(--sc-blue-40,#2563eb);cursor:pointer;font-size:.875rem;margin-top:.5rem;padding:.4em 1em}.fields-empty{color:#9ca3af;font-style:italic;padding:1rem 0}
339
+ .fields-panel{padding:.5em 1em}.fields-panel :deep(.atable-row>td){border-top:1px solid var(--sc-row-border-color);padding:var(--sc-atable-row-padding) .75em;vertical-align:middle}.fields-panel :deep(input[type=text]),.fields-panel :deep(select){border:1px solid var(--sc-gray-20);border-radius:var(--sc-border-radius);font-family:inherit;font-size:inherit;padding:.25em .5em;width:100%}.fields-panel :deep(input.locked),.fields-panel :deep(select.locked){background:var(--sc-gray-10);color:var(--sc-header-text-color);cursor:not-allowed}.fields-panel :deep(input.json-invalid){background:var(--sc-badge-danger-bg);border-color:var(--sc-brand-danger)}.center{text-align:center}.badge{border-radius:9999px;display:inline-block;font-size:.75rem;font-weight:500;padding:.125em .5em}.badge-manual{background:var(--sc-badge-brand-bg);color:var(--sc-badge-brand-text)}.badge-introspected{background:var(--sc-badge-neutral-bg);color:var(--sc-badge-neutral-text)}.field-detail{display:grid;gap:.75rem 1rem;grid-template-columns:repeat(auto-fill,minmax(14rem,1fr));padding:.75rem 1rem}.field-prop{color:var(--sc-header-text-color);display:flex;flex-direction:column;font-size:.75rem;gap:.25rem}.field-prop-inline{align-items:center;flex-direction:row;gap:.5rem}.field-prop-wide{grid-column:1/-1}.field-detail-actions{align-items:center;display:flex;gap:1rem;justify-content:space-between;padding:0 1rem .75rem}.locked-note{color:var(--sc-header-text-color);font-size:.75rem;font-style:italic}.btn-add{background:none;border:1px dashed var(--sc-gray-20);border-radius:var(--sc-border-radius);color:var(--sc-primary-color);cursor:pointer;font-size:.875rem;margin-top:.5rem;padding:.4em 1em}.fields-empty{color:var(--sc-header-text-color);font-style:italic;padding:1rem 0}
340
340
  </style>
@@ -29,7 +29,7 @@
29
29
  <p class="empty-workflow-hint">No workflow yet. Name the first state to start building the workflow.</p>
30
30
  <div class="empty-workflow-form">
31
31
  <input v-model="newStateName" type="text" placeholder="e.g. Draft" @keyup.enter="seedWorkflow" />
32
- <button class="btn-seed" type="button" :disabled="!newStateName.trim()" @click="seedWorkflow">
32
+ <button class="btn-primary" type="button" :disabled="!newStateName.trim()" @click="seedWorkflow">
33
33
  Add first state
34
34
  </button>
35
35
  </div>
@@ -50,6 +50,9 @@
50
50
  </div>
51
51
  </div>
52
52
  </Desktop>
53
+ <template #fallback>
54
+ <div class="docbuilder-loading">Loading...</div>
55
+ </template>
53
56
  </ClientOnly>
54
57
  </template>
55
58
 
@@ -147,5 +150,5 @@ const docbuilderActions = computed(() => [
147
150
  </script>
148
151
 
149
152
  <style scoped>
150
- .docbuilder-desktop{height:100vh}.docbuilder-loading{padding:2rem;text-align:center}.docbuilder-page{box-sizing:border-box;padding:2rem}.builder-workflow{min-height:8rem;padding:.5em 1em}:deep(.node-editor){height:40vh;overflow:hidden;width:100%}.empty-workflow{padding:1rem 0}.empty-workflow-hint{color:#9ca3af;font-style:italic;margin:0 0 .75rem}.empty-workflow-form{align-items:center;display:flex;gap:.5rem}.empty-workflow-form input{border:1px solid var(--sc-gray-20,#d1d5db);border-radius:4px;font-family:inherit;font-size:.875rem;padding:.4em .6em}.btn-seed{background:var(--sc-blue-40,#3b82f6);border:none;border-radius:.4rem;color:#fff;cursor:pointer;font-size:.875rem;font-weight:500;padding:.45em 1em}.btn-seed:disabled{cursor:not-allowed;opacity:.5}.validation-panel{margin-bottom:1rem}.validation-errors{background:#fee2e2;border:1px solid #ef4444;border-radius:6px;color:#991b1b;margin-bottom:.5rem;padding:1rem}.validation-warnings{background:#fef9c3;border:1px solid #eab308;border-radius:6px;color:#713f12;padding:1rem}.dismiss-button{background:none;border:1px solid;border-radius:3px;cursor:pointer;font-size:.75rem;margin-left:1rem;padding:.125em .5em}.builder-actions{align-items:center;display:flex;gap:1rem;padding:1rem}.save-message{font-size:.875rem}.save-message.success{color:#065f46}.save-message.error{color:#991b1b}
153
+ .docbuilder-desktop{height:100vh}.docbuilder-loading{color:var(--sc-header-text-color);padding:2rem;text-align:center}.docbuilder-page{box-sizing:border-box;padding:2rem}.builder-workflow{min-height:8rem;padding:.5em 1em}:deep(.node-editor){height:40vh;overflow:hidden;width:100%}.empty-workflow{padding:1rem 0}.empty-workflow-hint{color:var(--sc-header-text-color);font-style:italic;margin:0 0 .75rem}.empty-workflow-form{align-items:center;display:flex;gap:.5rem}.empty-workflow-form input{border:1px solid var(--sc-input-border-color);border-radius:var(--sc-border-radius);font-family:inherit;font-size:.875rem;padding:.4em .6em}.btn-primary{background:var(--sc-primary-color);border:none;border-radius:var(--sc-border-radius);color:var(--sc-primary-text-color);cursor:pointer;font-size:.875rem;font-weight:500;padding:.45em 1em}.btn-primary:disabled{cursor:not-allowed;opacity:.5}.validation-panel{margin-bottom:1rem}.validation-errors{background:var(--sc-badge-danger-bg);border:1px solid var(--sc-badge-danger-accent);border-radius:var(--sc-border-radius);color:var(--sc-badge-danger-text);margin-bottom:.5rem;padding:1rem}.validation-warnings{background:var(--sc-badge-warning-bg);border:1px solid var(--sc-badge-warning-accent);border-radius:var(--sc-border-radius);color:var(--sc-badge-warning-text);padding:1rem}.dismiss-button{background:none;border:1px solid;border-radius:var(--sc-border-radius);cursor:pointer;font-size:.75rem;margin-left:1rem;padding:.125em .5em}.builder-actions{align-items:center;display:flex;gap:1rem;padding:1rem}.save-message{font-size:.875rem}.save-message.success{color:var(--sc-brand-success)}.save-message.error{color:var(--sc-brand-danger)}
151
154
  </style>
@@ -13,7 +13,7 @@
13
13
  placeholder="New doctype name (e.g. Invoice)"
14
14
  :disabled="creating"
15
15
  @keyup.enter="createDoctype" />
16
- <button type="button" class="btn-create" :disabled="creating || !newName.trim()" @click="createDoctype">
16
+ <button type="button" class="btn-primary" :disabled="creating || !newName.trim()" @click="createDoctype">
17
17
  {{ creating ? 'Creating\u2026' : '+ New DocType' }}
18
18
  </button>
19
19
  </div>
@@ -95,5 +95,5 @@ const indexActions = computed(() => [{ type: "button", label: "Home", action: ()
95
95
  </script>
96
96
 
97
97
  <style scoped>
98
- .docbuilder-desktop{height:100vh}.docbuilder-index-inner{margin:0 auto;max-width:1200px;padding:2rem}.docbuilder-header{padding:2rem 0 3rem;text-align:center}.docbuilder-header h1{font-size:2.5rem;font-weight:700;margin:0 0 1rem}.subtitle{color:#6b7280;font-size:1.125rem;margin:0}.empty,.loading{color:#6b7280;padding:2rem;text-align:center}.docbuilder-create{display:flex;gap:.5rem;margin-bottom:1rem}.docbuilder-create input{border:1px solid var(--sc-gray-20,#d1d5db);border-radius:4px;flex:1;font:inherit;padding:.5em .75em}.btn-create{background:var(--sc-blue-40,#3b82f6);border:none;border-radius:.4rem;color:#fff;cursor:pointer;font-weight:500;padding:.5em 1.25em;white-space:nowrap}.btn-create:disabled{cursor:not-allowed;opacity:.5}.create-error{color:#b91c1c;font-size:.875rem;margin:-.5rem 0 1rem}
98
+ .docbuilder-desktop{height:100vh}.docbuilder-index-inner{margin:0 auto;max-width:1200px;padding:2rem}.docbuilder-header{padding:2rem 0 3rem;text-align:center}.docbuilder-header h1{font-size:2.5rem;font-weight:700;margin:0 0 1rem}.subtitle{font-size:1.125rem;margin:0}.empty,.loading,.subtitle{color:var(--sc-header-text-color)}.empty,.loading{padding:2rem;text-align:center}.docbuilder-create{display:flex;gap:.5rem;margin-bottom:1rem}.docbuilder-create input{border:1px solid var(--sc-input-border-color);border-radius:var(--sc-border-radius);flex:1;font:inherit;padding:.5em .75em}.btn-primary{background:var(--sc-primary-color);border:none;border-radius:var(--sc-border-radius);color:var(--sc-primary-text-color);cursor:pointer;font-weight:500;padding:.5em 1.25em;white-space:nowrap}.btn-primary:disabled{cursor:not-allowed;opacity:.5}.create-error{color:var(--sc-brand-danger);font-size:.875rem;margin:-.5rem 0 1rem}
99
99
  </style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stonecrop/nuxt",
3
- "version": "0.34.0",
3
+ "version": "0.36.0",
4
4
  "description": "Nuxt module for Stonecrop",
5
5
  "keywords": [
6
6
  "nuxt",
@@ -60,18 +60,18 @@
60
60
  "pathe": "^2.0.3",
61
61
  "pinia": "^3.0.4",
62
62
  "prompts": "^2.4.2",
63
- "@stonecrop/aform": "0.34.0",
64
- "@stonecrop/casl-middleware": "0.34.0",
65
- "@stonecrop/atable": "0.34.0",
66
- "@stonecrop/code-editor": "0.34.0",
67
- "@stonecrop/graphql-client": "0.34.0",
68
- "@stonecrop/desktop": "0.34.0",
69
- "@stonecrop/node-editor": "0.34.0",
70
- "@stonecrop/graphql-middleware": "0.34.0",
71
- "@stonecrop/nuxt-grafserv": "0.34.0",
72
- "@stonecrop/schema": "0.34.0",
73
- "@stonecrop/stonecrop": "0.34.0",
74
- "@stonecrop/themes": "0.34.0"
63
+ "@stonecrop/atable": "0.36.0",
64
+ "@stonecrop/code-editor": "0.36.0",
65
+ "@stonecrop/casl-middleware": "0.36.0",
66
+ "@stonecrop/desktop": "0.36.0",
67
+ "@stonecrop/aform": "0.36.0",
68
+ "@stonecrop/graphql-client": "0.36.0",
69
+ "@stonecrop/graphql-middleware": "0.36.0",
70
+ "@stonecrop/nuxt-grafserv": "0.36.0",
71
+ "@stonecrop/schema": "0.36.0",
72
+ "@stonecrop/stonecrop": "0.36.0",
73
+ "@stonecrop/themes": "0.36.0",
74
+ "@stonecrop/node-editor": "0.36.0"
75
75
  },
76
76
  "devDependencies": {
77
77
  "@eslint/js": "^10.0.1",
@@ -95,6 +95,7 @@
95
95
  "nuxt": "^4.4.8",
96
96
  "nuxt-graphql-middleware": "^5.4.0",
97
97
  "postgraphile": "^5.0.3",
98
+ "temporal-polyfill": "^1.0.5",
98
99
  "typescript": "^6.0.3",
99
100
  "typescript-eslint": "^8.62.1",
100
101
  "vite": "^8.2.2",
@@ -26,11 +26,12 @@ export async function installFrontend(options: FrontendInstallerOptions): Promis
26
26
 
27
27
  try {
28
28
  // Add @stonecrop/nuxt and its required peer dependencies
29
- // These packages are imported by the Nuxt module and need to be available at runtime
29
+ // These packages are imported by the Nuxt module or the scaffolded app files and need to be available at runtime
30
30
  await addDependencies(cwd, {
31
31
  '@stonecrop/nuxt': 'latest',
32
32
  '@stonecrop/aform': 'latest',
33
33
  '@stonecrop/atable': 'latest',
34
+ '@stonecrop/graphql-client': 'latest',
34
35
  '@stonecrop/stonecrop': 'latest',
35
36
  '@stonecrop/node-editor': 'latest',
36
37
  '@stonecrop/schema': 'latest',
@@ -39,8 +39,10 @@ export async function installGrafserv(options: GrafservInstallerOptions): Promis
39
39
  await addDependencies(cwd, {
40
40
  '@stonecrop/nuxt-grafserv': 'latest',
41
41
  '@stonecrop/graphql-middleware': 'latest',
42
+ '@stonecrop/schema': 'latest',
42
43
  graphql: '^16.11.0',
43
44
  grafast: '^1.0.1',
45
+ 'temporal-polyfill': '^1.0.5',
44
46
  })
45
47
 
46
48
  // Update nuxt.config.ts
@@ -22,6 +22,7 @@ import { constant, lambda, loadOne, object } from 'grafast'
22
22
  import { getMeta, getAllMeta, applyGuardedTransition } from '@stonecrop/graphql-middleware'
23
23
  import { getRecordIdField } from '@stonecrop/schema'
24
24
  import type { DoctypeMeta } from '@stonecrop/schema'
25
+ import { Temporal } from 'temporal-polyfill'
25
26
  import { projects, tasks, type Project, type Task } from './data'
26
27
 
27
28
  // ============================================================
@@ -142,8 +143,8 @@ function nextId(doctype: string): string {
142
143
  // `selfTransition` has nothing for the dispatcher to apply, and without an entry here it fails
143
144
  // loudly rather than reporting a false success.
144
145
  //
145
- // Add your own by registering under the doctype's `name`. Throwing rejects the action; returning
146
- // the updated record makes it the client writeback payload.
146
+ // Add your own by registering under the doctype's `name`. Throwing rejects the action; what it
147
+ // returns becomes the result's `data`, while the client stores the record as a read returns it.
147
148
 
148
149
  type ActionHandler = (context: {
149
150
  recordId?: string
@@ -165,9 +166,9 @@ export const actionHandlers: Record<string, Record<string, ActionHandler>> = {
165
166
  const task = recordId != null ? tasks.get(recordId) : undefined
166
167
  if (!task) throw new Error(`Task ${recordId ?? '(none)'} not found`)
167
168
 
168
- const from = task.dueDate ? new Date(task.dueDate) : new Date()
169
- from.setDate(from.getDate() + 7)
170
- const updated: Task = { ...task, dueDate: from.toISOString().slice(0, 10) }
169
+ // A task with no due date is snoozed from the server's today.
170
+ const from = task.dueDate ? Temporal.PlainDate.from(task.dueDate) : Temporal.Now.plainDateISO()
171
+ const updated: Task = { ...task, dueDate: from.add({ weeks: 1 }).toString() }
171
172
  tasks.set(task.id, updated)
172
173
  return Promise.resolve(updated)
173
174
  },
@@ -278,7 +279,7 @@ export const resolvers = {
278
279
  // The server owns the transition: read current state, guard against allowedStates,
279
280
  // write nextState. Reads/writes go straight to the in-memory Maps; a PostGraphile
280
281
  // setup swaps in pgClient SQL instead.
281
- return await applyGuardedTransition(
282
+ const outcome = await applyGuardedTransition(
282
283
  actionDef,
283
284
  {
284
285
  readState: async () => {
@@ -302,7 +303,7 @@ export const resolvers = {
302
303
  // untouched); creating derives the identity from the doctype's declared
303
304
  // primary key when the submitted data carries it, which is how a
304
305
  // natural-keyed doctype is identified, and mints one only otherwise.
305
- // Either way the full record comes back for the client writeback.
306
+ // Either way the full record comes back, stating its identity.
306
307
  writeData: async (patch: Record<string, unknown>, exists: boolean) => {
307
308
  if (exists) {
308
309
  if (recordId == null) return {}
@@ -340,6 +341,15 @@ export const resolvers = {
340
341
  },
341
342
  recordData
342
343
  )
344
+ // The record as `stonecropRecord` returns it: the client stores this, never
345
+ // `data`. Keyed by the identity the reply states, since a save may create it.
346
+ const repliedId: unknown =
347
+ outcome.data !== null && typeof outcome.data === 'object'
348
+ ? Reflect.get(outcome.data, lookupField)
349
+ : undefined
350
+ const readId = repliedId ?? recordId
351
+ const record = outcome.success && readId != null ? getRecord(d, String(readId), lookupField) : null
352
+ return { ...outcome, record }
343
353
  } catch (err) {
344
354
  return { success: false, data: null, error: err instanceof Error ? err.message : String(err) }
345
355
  }
@@ -34,6 +34,7 @@ type StonecropField {
34
34
  validation: JSON
35
35
  cardinality: String
36
36
  source: String
37
+ config: JSON
37
38
  }
38
39
 
39
40
  """
@@ -100,6 +101,8 @@ type ActionResult {
100
101
  success: Boolean!
101
102
  data: JSON
102
103
  error: String
104
+ "The record as stonecropRecord returns it after the action. Null when the action failed or targets no record."
105
+ record: JSON
103
106
  }
104
107
 
105
108
  # ============================================