@nixxie-cms/core 1.0.3 → 1.1.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 (202) hide show
  1. package/CHANGES-1.1.md +134 -0
  2. package/context/dist/nixxie-cms-core-context.cjs.js +4 -3
  3. package/context/dist/nixxie-cms-core-context.esm.js +3 -2
  4. package/dist/declarations/src/access.d.ts +2 -2
  5. package/dist/declarations/src/access.d.ts.map +1 -1
  6. package/dist/declarations/src/admin-ui/components/Navigation.d.ts +2 -2
  7. package/dist/declarations/src/admin-ui/components/Navigation.d.ts.map +1 -1
  8. package/dist/declarations/src/admin-ui/context.d.ts +6 -6
  9. package/dist/declarations/src/admin-ui/context.d.ts.map +1 -1
  10. package/dist/declarations/src/admin-ui/utils/Fields.d.ts +3 -3
  11. package/dist/declarations/src/admin-ui/utils/Fields.d.ts.map +1 -1
  12. package/dist/declarations/src/admin-ui/utils/filters.d.ts +5 -5
  13. package/dist/declarations/src/admin-ui/utils/filters.d.ts.map +1 -1
  14. package/dist/declarations/src/admin-ui/utils/useCreateItem.d.ts +3 -3
  15. package/dist/declarations/src/admin-ui/utils/useCreateItem.d.ts.map +1 -1
  16. package/dist/declarations/src/admin-ui/utils/utils.d.ts +2 -2
  17. package/dist/declarations/src/admin-ui/utils/utils.d.ts.map +1 -1
  18. package/dist/declarations/src/context.d.ts +1 -1
  19. package/dist/declarations/src/context.d.ts.map +1 -1
  20. package/dist/declarations/src/fields/types/bigInt/index.d.ts +3 -3
  21. package/dist/declarations/src/fields/types/bigInt/index.d.ts.map +1 -1
  22. package/dist/declarations/src/fields/types/bytes/index.d.ts +3 -3
  23. package/dist/declarations/src/fields/types/bytes/index.d.ts.map +1 -1
  24. package/dist/declarations/src/fields/types/calendarDay/index.d.ts +3 -3
  25. package/dist/declarations/src/fields/types/calendarDay/index.d.ts.map +1 -1
  26. package/dist/declarations/src/fields/types/checkbox/index.d.ts +3 -3
  27. package/dist/declarations/src/fields/types/checkbox/index.d.ts.map +1 -1
  28. package/dist/declarations/src/fields/types/decimal/index.d.ts +3 -3
  29. package/dist/declarations/src/fields/types/decimal/index.d.ts.map +1 -1
  30. package/dist/declarations/src/fields/types/file/index.d.ts +4 -4
  31. package/dist/declarations/src/fields/types/file/index.d.ts.map +1 -1
  32. package/dist/declarations/src/fields/types/float/index.d.ts +3 -3
  33. package/dist/declarations/src/fields/types/float/index.d.ts.map +1 -1
  34. package/dist/declarations/src/fields/types/image/index.d.ts +4 -4
  35. package/dist/declarations/src/fields/types/image/index.d.ts.map +1 -1
  36. package/dist/declarations/src/fields/types/integer/index.d.ts +3 -3
  37. package/dist/declarations/src/fields/types/integer/index.d.ts.map +1 -1
  38. package/dist/declarations/src/fields/types/json/index.d.ts +3 -3
  39. package/dist/declarations/src/fields/types/json/index.d.ts.map +1 -1
  40. package/dist/declarations/src/fields/types/multiselect/index.d.ts +3 -3
  41. package/dist/declarations/src/fields/types/multiselect/index.d.ts.map +1 -1
  42. package/dist/declarations/src/fields/types/multiselect/views/index.d.ts.map +1 -1
  43. package/dist/declarations/src/fields/types/password/index.d.ts +3 -3
  44. package/dist/declarations/src/fields/types/password/index.d.ts.map +1 -1
  45. package/dist/declarations/src/fields/types/relationship/index.d.ts +8 -8
  46. package/dist/declarations/src/fields/types/relationship/index.d.ts.map +1 -1
  47. package/dist/declarations/src/fields/types/relationship/views/ComboboxMany.d.ts +3 -3
  48. package/dist/declarations/src/fields/types/relationship/views/ComboboxMany.d.ts.map +1 -1
  49. package/dist/declarations/src/fields/types/relationship/views/ComboboxSingle.d.ts +3 -3
  50. package/dist/declarations/src/fields/types/relationship/views/ComboboxSingle.d.ts.map +1 -1
  51. package/dist/declarations/src/fields/types/relationship/views/index.d.ts +3 -3
  52. package/dist/declarations/src/fields/types/relationship/views/index.d.ts.map +1 -1
  53. package/dist/declarations/src/fields/types/relationship/views/types.d.ts +3 -3
  54. package/dist/declarations/src/fields/types/relationship/views/types.d.ts.map +1 -1
  55. package/dist/declarations/src/fields/types/select/index.d.ts +3 -3
  56. package/dist/declarations/src/fields/types/select/index.d.ts.map +1 -1
  57. package/dist/declarations/src/fields/types/text/index.d.ts +3 -3
  58. package/dist/declarations/src/fields/types/text/index.d.ts.map +1 -1
  59. package/dist/declarations/src/fields/types/timestamp/index.d.ts +3 -3
  60. package/dist/declarations/src/fields/types/timestamp/index.d.ts.map +1 -1
  61. package/dist/declarations/src/fields/types/virtual/index.d.ts +7 -7
  62. package/dist/declarations/src/fields/types/virtual/index.d.ts.map +1 -1
  63. package/dist/declarations/src/helpers.d.ts +249 -13
  64. package/dist/declarations/src/helpers.d.ts.map +1 -1
  65. package/dist/declarations/src/index.d.ts +9 -4
  66. package/dist/declarations/src/index.d.ts.map +1 -1
  67. package/dist/declarations/src/internal-unstable/admin-ui/pages/ListPage/index.d.ts.map +1 -1
  68. package/dist/declarations/src/lib/admin-meta.d.ts +11 -11
  69. package/dist/declarations/src/lib/admin-meta.d.ts.map +1 -1
  70. package/dist/declarations/src/lib/core/access-control.d.ts +18 -18
  71. package/dist/declarations/src/lib/core/access-control.d.ts.map +1 -1
  72. package/dist/declarations/src/lib/core/cascade.d.ts +47 -0
  73. package/dist/declarations/src/lib/core/cascade.d.ts.map +1 -0
  74. package/dist/declarations/src/lib/core/initialise-lists.d.ts +27 -24
  75. package/dist/declarations/src/lib/core/initialise-lists.d.ts.map +1 -1
  76. package/dist/declarations/src/lib/env.d.ts +9 -0
  77. package/dist/declarations/src/lib/env.d.ts.map +1 -0
  78. package/dist/declarations/src/lib/system.d.ts +1 -1
  79. package/dist/declarations/src/lib/system.d.ts.map +1 -1
  80. package/dist/declarations/src/list-features.d.ts +162 -0
  81. package/dist/declarations/src/list-features.d.ts.map +1 -0
  82. package/dist/declarations/src/schema.d.ts +24 -23
  83. package/dist/declarations/src/schema.d.ts.map +1 -1
  84. package/dist/declarations/src/session.d.ts +75 -0
  85. package/dist/declarations/src/session.d.ts.map +1 -1
  86. package/dist/declarations/src/types/admin-meta.d.ts +11 -11
  87. package/dist/declarations/src/types/admin-meta.d.ts.map +1 -1
  88. package/dist/declarations/src/types/config/access-control.d.ts +42 -42
  89. package/dist/declarations/src/types/config/access-control.d.ts.map +1 -1
  90. package/dist/declarations/src/types/config/fields.d.ts +19 -19
  91. package/dist/declarations/src/types/config/fields.d.ts.map +1 -1
  92. package/dist/declarations/src/types/config/hooks.d.ts +131 -131
  93. package/dist/declarations/src/types/config/hooks.d.ts.map +1 -1
  94. package/dist/declarations/src/types/config/index.d.ts +171 -8
  95. package/dist/declarations/src/types/config/index.d.ts.map +1 -1
  96. package/dist/declarations/src/types/config/lists.d.ts +146 -108
  97. package/dist/declarations/src/types/config/lists.d.ts.map +1 -1
  98. package/dist/declarations/src/types/context.d.ts +349 -47
  99. package/dist/declarations/src/types/context.d.ts.map +1 -1
  100. package/dist/declarations/src/types/next-fields.d.ts +28 -28
  101. package/dist/declarations/src/types/next-fields.d.ts.map +1 -1
  102. package/dist/declarations/src/types/type-info.d.ts +3 -3
  103. package/dist/declarations/src/types/type-info.d.ts.map +1 -1
  104. package/dist/{express-7559ca2d.esm.js → express-0abbce07.esm.js} +6 -6
  105. package/dist/{express-455ae20c.cjs.js → express-7ca6f76a.cjs.js} +6 -6
  106. package/dist/{index-15c8f81e.esm.js → index-5d8b0b4e.esm.js} +363 -183
  107. package/dist/index-6055753b.cjs.js +393 -0
  108. package/dist/{index-42045902.cjs.js → index-ac29f382.cjs.js} +363 -185
  109. package/dist/index-f1703b7b.esm.js +386 -0
  110. package/dist/nixxie-cms-core.cjs.js +1387 -30
  111. package/dist/nixxie-cms-core.esm.js +1361 -24
  112. package/dist/{non-null-graphql-add6bb3d.cjs.js → non-null-graphql-4a44c122.cjs.js} +1 -1
  113. package/dist/{non-null-graphql-a84ed64d.esm.js → non-null-graphql-8c5feaae.esm.js} +1 -1
  114. package/dist/{resolve-hooks-165a9ce2.cjs.js → resolve-hooks-10a5f84c.cjs.js} +240 -6
  115. package/dist/{resolve-hooks-6813a045.esm.js → resolve-hooks-9e676794.esm.js} +238 -7
  116. package/dist/{system-03e49e4f.esm.js → system-4d2a2648.esm.js} +32 -7
  117. package/dist/{system-a321642d.cjs.js → system-69e1a285.cjs.js} +32 -7
  118. package/fields/dist/nixxie-cms-core-fields.cjs.js +29 -576
  119. package/fields/dist/nixxie-cms-core-fields.esm.js +18 -565
  120. package/fields/types/bytes/dist/nixxie-cms-core-fields-types-bytes.cjs.js +4 -2
  121. package/fields/types/bytes/dist/nixxie-cms-core-fields-types-bytes.esm.js +4 -2
  122. package/fields/types/multiselect/views/dist/nixxie-cms-core-fields-types-multiselect-views.cjs.js +1 -6
  123. package/fields/types/multiselect/views/dist/nixxie-cms-core-fields-types-multiselect-views.esm.js +1 -6
  124. package/fields/types/password/dist/nixxie-cms-core-fields-types-password.cjs.js +4 -2
  125. package/fields/types/password/dist/nixxie-cms-core-fields-types-password.esm.js +4 -2
  126. package/internal-unstable/artifacts/dist/nixxie-cms-core-internal-unstable-artifacts.cjs.js +4 -3
  127. package/internal-unstable/artifacts/dist/nixxie-cms-core-internal-unstable-artifacts.esm.js +4 -3
  128. package/package.json +4 -4
  129. package/scripts/cli/dist/nixxie-cms-core-scripts-cli.cjs.js +4 -3
  130. package/scripts/cli/dist/nixxie-cms-core-scripts-cli.esm.js +4 -3
  131. package/scripts/dist/nixxie-cms-core-scripts.cjs.js +4 -3
  132. package/scripts/dist/nixxie-cms-core-scripts.esm.js +4 -3
  133. package/session/dist/nixxie-cms-core-session.cjs.js +286 -0
  134. package/session/dist/nixxie-cms-core-session.esm.js +279 -1
  135. package/src/access.ts +25 -25
  136. package/src/admin-ui/admin-meta-graphql.ts +5 -5
  137. package/src/admin-ui/components/CreateButtonLink.tsx +46 -46
  138. package/src/admin-ui/components/Navigation.tsx +3 -3
  139. package/src/admin-ui/context.tsx +6 -6
  140. package/src/admin-ui/utils/Fields.tsx +241 -241
  141. package/src/admin-ui/utils/actionData.ts +36 -36
  142. package/src/admin-ui/utils/filters.ts +148 -148
  143. package/src/admin-ui/utils/useCreateItem.ts +171 -171
  144. package/src/admin-ui/utils/utils.tsx +127 -127
  145. package/src/context.ts +1 -1
  146. package/src/fields/non-null-graphql.ts +115 -115
  147. package/src/fields/types/bigInt/index.ts +6 -6
  148. package/src/fields/types/bytes/index.ts +6 -6
  149. package/src/fields/types/calendarDay/index.ts +18 -19
  150. package/src/fields/types/checkbox/index.ts +6 -6
  151. package/src/fields/types/decimal/index.ts +6 -6
  152. package/src/fields/types/file/index.ts +8 -8
  153. package/src/fields/types/float/index.ts +6 -6
  154. package/src/fields/types/image/index.ts +8 -8
  155. package/src/fields/types/integer/index.ts +6 -6
  156. package/src/fields/types/json/index.ts +5 -5
  157. package/src/fields/types/multiselect/index.ts +7 -7
  158. package/src/fields/types/multiselect/views/index.tsx +149 -151
  159. package/src/fields/types/password/index.ts +6 -6
  160. package/src/fields/types/relationship/index.ts +13 -13
  161. package/src/fields/types/relationship/views/ComboboxMany.tsx +110 -110
  162. package/src/fields/types/relationship/views/ComboboxSingle.tsx +115 -115
  163. package/src/fields/types/relationship/views/ContextualActions.tsx +139 -139
  164. package/src/fields/types/relationship/views/index.tsx +492 -492
  165. package/src/fields/types/relationship/views/types.ts +46 -46
  166. package/src/fields/types/relationship/views/useApolloQuery.ts +185 -185
  167. package/src/fields/types/relationship/views/useFilter.tsx +109 -109
  168. package/src/fields/types/select/index.ts +6 -6
  169. package/src/fields/types/text/index.ts +6 -6
  170. package/src/fields/types/timestamp/index.ts +23 -21
  171. package/src/fields/types/virtual/index.ts +11 -11
  172. package/src/helpers.ts +773 -42
  173. package/src/index.ts +66 -24
  174. package/src/internal-unstable/admin-ui/pages/ItemPage/common.tsx +4 -4
  175. package/src/internal-unstable/admin-ui/pages/ItemPage/index.tsx +5 -5
  176. package/src/internal-unstable/admin-ui/pages/ListPage/index.tsx +8 -8
  177. package/src/lib/admin-meta.ts +369 -369
  178. package/src/lib/context/createContext.ts +5 -0
  179. package/src/lib/core/access-control.ts +434 -434
  180. package/src/lib/core/cascade.ts +236 -0
  181. package/src/lib/core/initialise-lists.ts +49 -33
  182. package/src/lib/core/mutations/index.ts +7 -0
  183. package/src/lib/core/mutations/nested-mutation-many-input-resolvers.ts +145 -145
  184. package/src/lib/core/mutations/nested-mutation-one-input-resolvers.ts +71 -71
  185. package/src/lib/core/queries/output-field.ts +178 -178
  186. package/src/lib/env.ts +50 -0
  187. package/src/lib/id-field.ts +2 -2
  188. package/src/lib/system.ts +221 -207
  189. package/src/lib/typescript-schema-printer.ts +227 -227
  190. package/src/list-features.ts +476 -0
  191. package/src/schema.ts +91 -22
  192. package/src/session.ts +225 -0
  193. package/src/types/admin-meta.ts +218 -218
  194. package/src/types/config/access-control.ts +186 -186
  195. package/src/types/config/fields.ts +96 -96
  196. package/src/types/config/hooks.ts +529 -529
  197. package/src/types/config/index.ts +185 -7
  198. package/src/types/config/lists.ts +606 -565
  199. package/src/types/context.ts +426 -55
  200. package/src/types/next-fields.ts +31 -31
  201. package/src/types/type-info.ts +38 -38
  202. package/src/types/type-tests.ts +21 -21
@@ -3,7 +3,7 @@ import type { DocumentNode, ExecutionResult, GraphQLSchema } from 'graphql'
3
3
  import type { TypedDocumentNode } from '@graphql-typed-document-node/core'
4
4
  import type { InitialisedList } from '../lib/core/initialise-lists'
5
5
  import type { SessionStrategy } from './session'
6
- import type { BaseNixxieTypeInfo, BaseListTypeInfo } from './type-info'
6
+ import type { BaseNixxieTypeInfo, BaseCollectionTypeInfo } from './type-info'
7
7
  import type { MaybePromise } from './utils'
8
8
 
9
9
  // ── Email service types (defined here so @nixxie-cms/email can implement without circular deps) ──
@@ -94,20 +94,41 @@ export type NixxieEmailService = {
94
94
  close(): Promise<void>
95
95
  }
96
96
 
97
+ // ── Service lifecycle ──
98
+
99
+ /**
100
+ * Optional lifecycle hook for runtime services. When a service object exposes `init`,
101
+ * core calls it once with a sudo context after the database connects — this is how
102
+ * database-backed service stores (audit, versioning, workflow, jobs history, webhook
103
+ * subscriptions) get access to `context.prisma` without a circular boot dependency.
104
+ */
105
+ export type NixxieInitableService = {
106
+ init?(context: NixxieContext): MaybePromise<void>
107
+ }
108
+
97
109
  // ── Jobs service ──
98
110
 
99
111
  export type NixxieJobDefinition = {
100
112
  name: string
101
- /** Cron expression or interval in milliseconds */
102
- schedule: string | number
113
+ /**
114
+ * Cron expression or interval in milliseconds.
115
+ * Omit when `runAt` is set (one-time job). One of `schedule` or `runAt` is required.
116
+ */
117
+ schedule?: string | number
118
+ /** Run once at this time instead of on a schedule. Times in the past run immediately. */
119
+ runAt?: Date
103
120
  /** Handler function invoked on each tick */
104
- handler: (ctx: { jobId: string; scheduledAt: Date }) => Promise<void> | void
121
+ handler: (ctx: { jobId: string; scheduledAt: Date; attempt: number }) => Promise<void> | void
105
122
  /** Immediately run the job once on registration */
106
123
  runImmediately?: boolean
107
124
  /** Disable job without removing the definition */
108
125
  enabled?: boolean
109
126
  /** Milliseconds before a running job is considered stuck */
110
127
  timeout?: number
128
+ /** Number of retry attempts after a failed run (0 = no retries) */
129
+ retries?: number
130
+ /** Base delay in milliseconds between retries (doubles each attempt) */
131
+ retryDelay?: number
111
132
  }
112
133
 
113
134
  export type NixxieJobStatus = 'idle' | 'running' | 'error' | 'disabled'
@@ -121,7 +142,7 @@ export type NixxieJobInfo = {
121
142
  runs: number
122
143
  }
123
144
 
124
- export type NixxieJobsService = {
145
+ export type NixxieJobsService = NixxieInitableService & {
125
146
  /** Register a job definition. Call before start() or at any time for hot-registration. */
126
147
  define(job: NixxieJobDefinition): void
127
148
  /** Remove a registered job by name. */
@@ -215,7 +236,7 @@ export type NixxieAuditRecord = NixxieAuditEntry & {
215
236
  timestamp: Date
216
237
  }
217
238
 
218
- export type NixxieAuditService = {
239
+ export type NixxieAuditService = NixxieInitableService & {
219
240
  /** Log an audit event. */
220
241
  log(entry: NixxieAuditEntry): Promise<NixxieAuditRecord>
221
242
  /** Query the audit log. */
@@ -261,9 +282,13 @@ export type NixxieWebhookSubscribeOptions = {
261
282
  retries?: number
262
283
  }
263
284
 
264
- export type NixxieWebhooksService = {
285
+ export type NixxieWebhooksService = NixxieInitableService & {
265
286
  /** Register a webhook endpoint for an event. Returns the subscription ID. */
266
- subscribe(event: string, url: string, options?: NixxieWebhookSubscribeOptions): Promise<NixxieWebhookSubscription>
287
+ subscribe(
288
+ event: string,
289
+ url: string,
290
+ options?: NixxieWebhookSubscribeOptions
291
+ ): Promise<NixxieWebhookSubscription>
267
292
  /** Remove a subscription by ID. */
268
293
  unsubscribe(id: string): Promise<void>
269
294
  /** Fire an event, delivering to all matching subscribers. */
@@ -316,7 +341,11 @@ export type NixxieRateLimitService = {
316
341
 
317
342
  // ── Health service ──
318
343
 
319
- export type NixxieHealthCheckFn = () => Promise<{ ok: boolean; message?: string; details?: unknown }>
344
+ export type NixxieHealthCheckFn = () => Promise<{
345
+ ok: boolean
346
+ message?: string
347
+ details?: unknown
348
+ }>
320
349
 
321
350
  export type NixxieHealthCheckResult = {
322
351
  name: string
@@ -396,7 +425,11 @@ export type NixxieSignedUrlOptions = {
396
425
  /** Pluggable blob storage. Implemented by @nixxie-cms/storage (local, S3, GCS, Azure). */
397
426
  export type NixxieStorageService = {
398
427
  /** Store a file and return its key + URL. */
399
- put(key: string, data: Buffer | Uint8Array | string, options?: NixxiePutOptions): Promise<NixxieStoredFile>
428
+ put(
429
+ key: string,
430
+ data: Buffer | Uint8Array | string,
431
+ options?: NixxiePutOptions
432
+ ): Promise<NixxieStoredFile>
400
433
  /** Read a file's bytes. Resolves undefined when the key does not exist. */
401
434
  get(key: string): Promise<Buffer | undefined>
402
435
  /** Delete a file. No-op if it does not exist. */
@@ -449,7 +482,10 @@ export type NixxieSearchService = {
449
482
  /** Remove a document from an index by id. */
450
483
  remove(indexName: string, id: string): Promise<void>
451
484
  /** Run a search against an index. */
452
- search<T = NixxieSearchDocument>(indexName: string, query: NixxieSearchQuery): Promise<NixxieSearchResults<T>>
485
+ search<T = NixxieSearchDocument>(
486
+ indexName: string,
487
+ query: NixxieSearchQuery
488
+ ): Promise<NixxieSearchResults<T>>
453
489
  /** Delete every document in an index. */
454
490
  clear(indexName: string): Promise<void>
455
491
  /** Gracefully close any open connections. */
@@ -526,11 +562,341 @@ export type NixxieAiService = {
526
562
  embedMany(texts: string[]): Promise<number[][]>
527
563
  }
528
564
 
565
+ // ── Versioning service ──
566
+
567
+ export type NixxieVersionEntry = {
568
+ /** Collection name (e.g. 'Post') */
569
+ resource: string
570
+ /** ID of the item this snapshot belongs to */
571
+ resourceId: string
572
+ /** Snapshot of the item's data at this point in time */
573
+ data: Record<string, unknown>
574
+ /** Optional human label (e.g. 'before publish') */
575
+ label?: string
576
+ /** Who created the snapshot */
577
+ actor?: {
578
+ id?: string
579
+ label?: string
580
+ }
581
+ /** Arbitrary extra context */
582
+ meta?: Record<string, unknown>
583
+ }
584
+
585
+ export type NixxieVersionRecord = NixxieVersionEntry & {
586
+ id: string
587
+ /** Monotonically increasing version number, scoped to resource + resourceId (starts at 1). */
588
+ version: number
589
+ createdAt: Date
590
+ }
591
+
592
+ export type NixxieVersionQuery = {
593
+ take?: number
594
+ skip?: number
595
+ }
596
+
597
+ export type NixxieVersionDiff = {
598
+ fromVersion: number
599
+ toVersion: number
600
+ /** Per-field differences between the two versions. */
601
+ changes: Record<string, { from: unknown; to: unknown }>
602
+ }
603
+
604
+ /** Content revision history. Implemented by @nixxie-cms/versioning. */
605
+ export type NixxieVersioningService = NixxieInitableService & {
606
+ /** Record a new snapshot of an item's data. Returns the created version record. */
607
+ snapshot(entry: NixxieVersionEntry): Promise<NixxieVersionRecord>
608
+ /** List versions for an item, newest first. */
609
+ list(
610
+ resource: string,
611
+ resourceId: string,
612
+ query?: NixxieVersionQuery
613
+ ): Promise<NixxieVersionRecord[]>
614
+ /** Fetch a single version by its ID. */
615
+ get(versionId: string): Promise<NixxieVersionRecord | undefined>
616
+ /** Get the most recent version for an item. */
617
+ latest(resource: string, resourceId: string): Promise<NixxieVersionRecord | undefined>
618
+ /**
619
+ * Restore an older version: copies its data into a brand-new snapshot
620
+ * (so history is never lost) and returns that new record. The caller is
621
+ * responsible for writing `record.data` back to the live item.
622
+ */
623
+ restore(versionId: string): Promise<NixxieVersionRecord>
624
+ /** Compute a field-level diff between two versions of the same item. */
625
+ diff(fromVersionId: string, toVersionId: string): Promise<NixxieVersionDiff>
626
+ /** Keep only the newest `keep` versions of an item, deleting the rest. Returns the number deleted. */
627
+ prune(resource: string, resourceId: string, keep: number): Promise<number>
628
+ /** Gracefully shut down (flush pending writes). */
629
+ close(): Promise<void>
630
+ }
631
+
632
+ // ── Workflow service ──
633
+
634
+ export type NixxieWorkflowState = string
635
+
636
+ export type NixxieWorkflowTransition = {
637
+ /** The state(s) this transition can start from. */
638
+ from: NixxieWorkflowState | NixxieWorkflowState[]
639
+ /** The state this transition moves to. */
640
+ to: NixxieWorkflowState
641
+ /** Optional action name (e.g. 'submit', 'approve', 'reject'). */
642
+ name?: string
643
+ /**
644
+ * Optional guard evaluated before the transition is recorded.
645
+ * Return `false` to block it, or a string to block it with a custom error message.
646
+ */
647
+ when?: (args: {
648
+ resource: string
649
+ resourceId: string
650
+ from: NixxieWorkflowState
651
+ to: NixxieWorkflowState
652
+ actor?: NixxieWorkflowActor
653
+ meta?: Record<string, unknown>
654
+ }) => MaybePromise<boolean | string>
655
+ }
656
+
657
+ export type NixxieWorkflowDefinition = {
658
+ name: string
659
+ /** State assigned to items that have no recorded state yet. */
660
+ initial: NixxieWorkflowState
661
+ /** All valid states. */
662
+ states: NixxieWorkflowState[]
663
+ /** Allowed transitions between states. */
664
+ transitions: NixxieWorkflowTransition[]
665
+ }
666
+
667
+ export type NixxieWorkflowActor = {
668
+ id?: string
669
+ label?: string
670
+ }
671
+
672
+ export type NixxieWorkflowTransitionInput = {
673
+ actor?: NixxieWorkflowActor
674
+ meta?: Record<string, unknown>
675
+ }
676
+
677
+ export type NixxieWorkflowHistoryEntry = {
678
+ id: string
679
+ resource: string
680
+ resourceId: string
681
+ /** Previous state, or null for the first transition. */
682
+ from: NixxieWorkflowState | null
683
+ to: NixxieWorkflowState
684
+ actor?: NixxieWorkflowActor
685
+ meta?: Record<string, unknown>
686
+ at: Date
687
+ }
688
+
689
+ /** Editorial state machine (draft → review → published, etc). Implemented by @nixxie-cms/workflow. */
690
+ export type NixxieWorkflowService = NixxieInitableService & {
691
+ /** The workflow definition this service enforces. */
692
+ definition(): NixxieWorkflowDefinition
693
+ /** Current state of an item (returns the initial state if none recorded). */
694
+ getState(resource: string, resourceId: string): Promise<NixxieWorkflowState>
695
+ /**
696
+ * Move an item to `to`, enforcing that a transition from its current state is allowed.
697
+ * Throws if the transition is not permitted.
698
+ */
699
+ transition(
700
+ resource: string,
701
+ resourceId: string,
702
+ to: NixxieWorkflowState,
703
+ input?: NixxieWorkflowTransitionInput
704
+ ): Promise<NixxieWorkflowHistoryEntry>
705
+ /** Force a state without checking transition rules (e.g. migrations / admin override). */
706
+ setState(
707
+ resource: string,
708
+ resourceId: string,
709
+ to: NixxieWorkflowState,
710
+ input?: NixxieWorkflowTransitionInput
711
+ ): Promise<NixxieWorkflowHistoryEntry>
712
+ /** Whether a direct transition from `from` to `to` is allowed. */
713
+ can(from: NixxieWorkflowState, to: NixxieWorkflowState): boolean
714
+ /** States reachable from `from` in a single transition. */
715
+ available(from: NixxieWorkflowState): NixxieWorkflowState[]
716
+ /** Transition history for an item, newest first. */
717
+ history(
718
+ resource: string,
719
+ resourceId: string,
720
+ take?: number
721
+ ): Promise<NixxieWorkflowHistoryEntry[]>
722
+ /** Gracefully shut down (flush pending writes). */
723
+ close(): Promise<void>
724
+ }
725
+
726
+ // ── API keys service ──
727
+
728
+ export type NixxieApiKeyIssueOptions = {
729
+ /** Human-friendly name for the key. */
730
+ name: string
731
+ /** Permission scopes granted to the key. */
732
+ scopes?: string[]
733
+ /** Optional expiry. Omit for a non-expiring key. */
734
+ expiresAt?: Date
735
+ /** Arbitrary extra context. */
736
+ meta?: Record<string, unknown>
737
+ }
738
+
739
+ export type NixxieApiKeyRecord = {
740
+ id: string
741
+ name: string
742
+ scopes: string[]
743
+ /** Short, non-secret prefix used to identify the key in UIs and logs. */
744
+ prefix: string
745
+ createdAt: Date
746
+ expiresAt?: Date
747
+ lastUsedAt?: Date
748
+ revokedAt?: Date
749
+ meta?: Record<string, unknown>
750
+ }
751
+
752
+ export type NixxieApiKeyIssued = {
753
+ record: NixxieApiKeyRecord
754
+ /** The full plaintext key. Shown ONCE at issue time — only its hash is stored. */
755
+ key: string
756
+ }
757
+
758
+ export type NixxieApiKeyVerifyResult =
759
+ | { valid: true; record: NixxieApiKeyRecord }
760
+ | { valid: false; reason: 'not-found' | 'revoked' | 'expired' | 'missing-scope' }
761
+
762
+ /** Scoped, revocable machine-to-machine access tokens. Implemented by @nixxie-cms/api-keys. */
763
+ export type NixxieApiKeysService = {
764
+ /** Issue a new key. The plaintext value is returned once and never stored. */
765
+ issue(options: NixxieApiKeyIssueOptions): Promise<NixxieApiKeyIssued>
766
+ /** Verify a plaintext key, checking revocation and expiry. Updates `lastUsedAt` on success. */
767
+ verify(key: string): Promise<NixxieApiKeyVerifyResult>
768
+ /** Verify a key and require that it holds every one of `scopes`. */
769
+ authorize(key: string, scopes: string[]): Promise<NixxieApiKeyVerifyResult>
770
+ /** Revoke a key by ID. */
771
+ revoke(id: string): Promise<void>
772
+ /** List all key records (never includes the plaintext key). */
773
+ list(): Promise<NixxieApiKeyRecord[]>
774
+ /** Fetch a single key record by ID. */
775
+ get(id: string): Promise<NixxieApiKeyRecord | undefined>
776
+ /** Gracefully shut down (flush pending writes). */
777
+ close(): Promise<void>
778
+ }
779
+
780
+ // ── Logger service ──
781
+
782
+ export type NixxieLogLevel = 'debug' | 'info' | 'warn' | 'error' | 'fatal'
783
+
784
+ export type NixxieLogFields = Record<string, unknown>
785
+
786
+ export type NixxieLogRecord = {
787
+ level: NixxieLogLevel
788
+ message: string
789
+ /** ISO-8601 timestamp. */
790
+ time: string
791
+ /** Bound + per-call structured fields merged together. */
792
+ fields: NixxieLogFields
793
+ }
794
+
795
+ /** Receives every emitted log record (e.g. console JSON, a file, an external sink). */
796
+ export type NixxieLogSink = (record: NixxieLogRecord) => void
797
+
798
+ /** Structured, level-aware logging with bound child loggers. Implemented by @nixxie-cms/logger. */
799
+ export type NixxieLoggerService = {
800
+ debug(message: string, fields?: NixxieLogFields): void
801
+ info(message: string, fields?: NixxieLogFields): void
802
+ warn(message: string, fields?: NixxieLogFields): void
803
+ error(message: string, fields?: NixxieLogFields): void
804
+ fatal(message: string, fields?: NixxieLogFields): void
805
+ /** Emit at an explicit level. */
806
+ log(level: NixxieLogLevel, message: string, fields?: NixxieLogFields): void
807
+ /**
808
+ * Create a child logger that always includes the given bound fields
809
+ * (e.g. `{ requestId }`). Children inherit the parent's level and sinks.
810
+ */
811
+ child(bindings: NixxieLogFields): NixxieLoggerService
812
+ /** The current minimum level; records below it are dropped. */
813
+ readonly level: NixxieLogLevel
814
+ }
815
+
816
+ // ── Backup service ──
817
+
818
+ export type NixxieBackupFormat = 'json' | 'ndjson'
819
+
820
+ export type NixxieBackupCollection = {
821
+ collection: string
822
+ items: Record<string, unknown>[]
823
+ }
824
+
825
+ export type NixxieBackupArchive = {
826
+ version: 1
827
+ createdAt: string
828
+ collections: NixxieBackupCollection[]
829
+ }
830
+
831
+ export type NixxieBackupExportOptions = {
832
+ /** Collections to include. Omit to export every collection the data source exposes. */
833
+ collections?: string[]
834
+ }
835
+
836
+ export type NixxieBackupImportMode = 'create' | 'upsert'
837
+
838
+ export type NixxieBackupImportOptions = {
839
+ /** 'create' (default) always inserts; 'upsert' updates existing items by id. */
840
+ mode?: NixxieBackupImportMode
841
+ /** Restrict the import to these collections. */
842
+ collections?: string[]
843
+ /** Continue past individual item failures instead of throwing. */
844
+ continueOnError?: boolean
845
+ }
846
+
847
+ export type NixxieBackupImportResult = {
848
+ created: number
849
+ updated: number
850
+ skipped: number
851
+ errors: { collection: string; id?: string; error: string }[]
852
+ }
853
+
854
+ /**
855
+ * Abstraction over the live data store, supplied by the host app (typically built
856
+ * from a NixxieContext). Lets the backup service read and write content without
857
+ * being coupled to Prisma or the GraphQL layer.
858
+ */
859
+ export type NixxieBackupDataSource = {
860
+ /** Names of the collections available for export/import. */
861
+ collections(): string[] | Promise<string[]>
862
+ /** Read every item of a collection. */
863
+ read(collection: string): Promise<Record<string, unknown>[]>
864
+ /** Write items into a collection, honouring the requested mode. */
865
+ write(
866
+ collection: string,
867
+ items: Record<string, unknown>[],
868
+ mode: NixxieBackupImportMode
869
+ ): Promise<
870
+ Omit<NixxieBackupImportResult, 'errors'> & { errors: { id?: string; error: string }[] }
871
+ >
872
+ /** Count items in a collection (used by `seed` to detect non-empty targets). */
873
+ count(collection: string): Promise<number>
874
+ }
875
+
876
+ /** Content export / import / seed. Implemented by @nixxie-cms/backup. */
877
+ export type NixxieBackupService = {
878
+ /** Read the data source into an in-memory archive. */
879
+ export(options?: NixxieBackupExportOptions): Promise<NixxieBackupArchive>
880
+ /** Serialize an archive to a string in the given format (default 'json'). */
881
+ serialize(archive: NixxieBackupArchive, format?: NixxieBackupFormat): string
882
+ /** Parse a serialized archive back into structured form. */
883
+ parse(text: string, format?: NixxieBackupFormat): NixxieBackupArchive
884
+ /** Write an archive into the data source. */
885
+ import(
886
+ archive: NixxieBackupArchive,
887
+ options?: NixxieBackupImportOptions
888
+ ): Promise<NixxieBackupImportResult>
889
+ /** Import an archive, but only into collections that are currently empty. */
890
+ seed(archive: NixxieBackupArchive): Promise<NixxieBackupImportResult>
891
+ /** Gracefully shut down. */
892
+ close(): Promise<void>
893
+ }
894
+
529
895
  // ── Context ──
530
896
 
531
897
  export type NixxieContext<TypeInfo extends BaseNixxieTypeInfo = BaseNixxieTypeInfo> = {
532
898
  db: NixxieDbAPI<TypeInfo['lists']>
533
- query: NixxieListsAPI<TypeInfo['lists']>
899
+ query: NixxieCollectionsAPI<TypeInfo['lists']>
534
900
  graphql: NixxieGraphQLAPI
535
901
  prisma: TypeInfo['prisma']
536
902
  /** Runtime services configured in nixxie.ts — available everywhere context is available */
@@ -546,6 +912,11 @@ export type NixxieContext<TypeInfo extends BaseNixxieTypeInfo = BaseNixxieTypeIn
546
912
  search: NixxieSearchService | null
547
913
  notifications: NixxieNotificationsService | null
548
914
  ai: NixxieAiService | null
915
+ versioning: NixxieVersioningService | null
916
+ workflow: NixxieWorkflowService | null
917
+ apiKeys: NixxieApiKeysService | null
918
+ logger: NixxieLoggerService | null
919
+ backup: NixxieBackupService | null
549
920
  }
550
921
  transaction: <T>(
551
922
  f: (context: NixxieContext<TypeInfo>) => MaybePromise<T>,
@@ -584,57 +955,57 @@ export type NixxieContext<TypeInfo extends BaseNixxieTypeInfo = BaseNixxieTypeIn
584
955
 
585
956
  // List item API
586
957
 
587
- type UniqueWhereInput<ListTypeInfo extends BaseListTypeInfo> =
588
- false extends ListTypeInfo['isSingleton']
589
- ? { readonly where: ListTypeInfo['inputs']['uniqueWhere'] }
590
- : { readonly where?: ListTypeInfo['inputs']['uniqueWhere'] }
958
+ type UniqueWhereInput<CollectionTypeInfo extends BaseCollectionTypeInfo> =
959
+ false extends CollectionTypeInfo['isSingleton']
960
+ ? { readonly where: CollectionTypeInfo['inputs']['uniqueWhere'] }
961
+ : { readonly where?: CollectionTypeInfo['inputs']['uniqueWhere'] }
591
962
 
592
- type ListAPI<ListTypeInfo extends BaseListTypeInfo> = {
963
+ type CollectionAPI<CollectionTypeInfo extends BaseCollectionTypeInfo> = {
593
964
  findMany(
594
965
  args?: {
595
- readonly where?: ListTypeInfo['inputs']['where']
966
+ readonly where?: CollectionTypeInfo['inputs']['where']
596
967
  readonly take?: number
597
968
  readonly skip?: number
598
969
  readonly orderBy?:
599
- | ListTypeInfo['inputs']['orderBy']
600
- | readonly ListTypeInfo['inputs']['orderBy'][]
601
- readonly cursor?: ListTypeInfo['inputs']['uniqueWhere']
970
+ | CollectionTypeInfo['inputs']['orderBy']
971
+ | readonly CollectionTypeInfo['inputs']['orderBy'][]
972
+ readonly cursor?: CollectionTypeInfo['inputs']['uniqueWhere']
602
973
  } & ResolveFields
603
974
  ): Promise<readonly Record<string, any>[]>
604
- findOne(args: UniqueWhereInput<ListTypeInfo> & ResolveFields): Promise<Record<string, any>>
605
- count(args?: { readonly where?: ListTypeInfo['inputs']['where'] }): Promise<number>
975
+ findOne(args: UniqueWhereInput<CollectionTypeInfo> & ResolveFields): Promise<Record<string, any>>
976
+ count(args?: { readonly where?: CollectionTypeInfo['inputs']['where'] }): Promise<number>
606
977
  updateOne(
607
- args: UniqueWhereInput<ListTypeInfo> & {
608
- readonly data: ListTypeInfo['inputs']['update']
978
+ args: UniqueWhereInput<CollectionTypeInfo> & {
979
+ readonly data: CollectionTypeInfo['inputs']['update']
609
980
  } & ResolveFields
610
981
  ): Promise<Record<string, any>>
611
982
  updateMany(
612
983
  args: {
613
- readonly data: readonly (UniqueWhereInput<ListTypeInfo> & {
614
- readonly data: ListTypeInfo['inputs']['update']
984
+ readonly data: readonly (UniqueWhereInput<CollectionTypeInfo> & {
985
+ readonly data: CollectionTypeInfo['inputs']['update']
615
986
  })[]
616
987
  } & ResolveFields
617
988
  ): Promise<Record<string, any>[]>
618
989
  createOne(
619
- args: { readonly data: ListTypeInfo['inputs']['create'] } & ResolveFields
990
+ args: { readonly data: CollectionTypeInfo['inputs']['create'] } & ResolveFields
620
991
  ): Promise<Record<string, any>>
621
992
  createMany(
622
993
  args: {
623
- readonly data: readonly ListTypeInfo['inputs']['create'][]
994
+ readonly data: readonly CollectionTypeInfo['inputs']['create'][]
624
995
  } & ResolveFields
625
996
  ): Promise<Record<string, any>[]>
626
997
  deleteOne(
627
- args: UniqueWhereInput<ListTypeInfo> & ResolveFields
998
+ args: UniqueWhereInput<CollectionTypeInfo> & ResolveFields
628
999
  ): Promise<Record<string, any> | null>
629
1000
  deleteMany(
630
1001
  args: {
631
- readonly where: readonly ListTypeInfo['inputs']['uniqueWhere'][]
1002
+ readonly where: readonly CollectionTypeInfo['inputs']['uniqueWhere'][]
632
1003
  } & ResolveFields
633
1004
  ): Promise<Record<string, any>[]>
634
1005
  }
635
1006
 
636
- export type NixxieListsAPI<ListsTypeInfo extends Record<string, BaseListTypeInfo>> = {
637
- [Key in keyof ListsTypeInfo]: ListAPI<ListsTypeInfo[Key]>
1007
+ export type NixxieCollectionsAPI<ListsTypeInfo extends Record<string, BaseCollectionTypeInfo>> = {
1008
+ [Key in keyof ListsTypeInfo]: CollectionAPI<ListsTypeInfo[Key]>
638
1009
  }
639
1010
 
640
1011
  type ResolveFields = {
@@ -644,41 +1015,41 @@ type ResolveFields = {
644
1015
  readonly query?: string
645
1016
  }
646
1017
 
647
- type DbAPI<ListTypeInfo extends BaseListTypeInfo> = {
1018
+ type DbAPI<CollectionTypeInfo extends BaseCollectionTypeInfo> = {
648
1019
  findMany(args?: {
649
- readonly where?: ListTypeInfo['inputs']['where']
1020
+ readonly where?: CollectionTypeInfo['inputs']['where']
650
1021
  readonly take?: number
651
1022
  readonly skip?: number
652
1023
  readonly orderBy?:
653
- | ListTypeInfo['inputs']['orderBy']
654
- | readonly ListTypeInfo['inputs']['orderBy'][]
655
- readonly cursor?: ListTypeInfo['inputs']['uniqueWhere']
656
- }): Promise<readonly ListTypeInfo['item'][]>
657
- findOne(args: UniqueWhereInput<ListTypeInfo>): Promise<ListTypeInfo['item'] | null>
658
- count(args?: { readonly where?: ListTypeInfo['inputs']['where'] }): Promise<number>
1024
+ | CollectionTypeInfo['inputs']['orderBy']
1025
+ | readonly CollectionTypeInfo['inputs']['orderBy'][]
1026
+ readonly cursor?: CollectionTypeInfo['inputs']['uniqueWhere']
1027
+ }): Promise<readonly CollectionTypeInfo['item'][]>
1028
+ findOne(args: UniqueWhereInput<CollectionTypeInfo>): Promise<CollectionTypeInfo['item'] | null>
1029
+ count(args?: { readonly where?: CollectionTypeInfo['inputs']['where'] }): Promise<number>
659
1030
  updateOne(
660
- args: UniqueWhereInput<ListTypeInfo> & {
661
- readonly data: ListTypeInfo['inputs']['update']
1031
+ args: UniqueWhereInput<CollectionTypeInfo> & {
1032
+ readonly data: CollectionTypeInfo['inputs']['update']
662
1033
  }
663
- ): Promise<ListTypeInfo['item']>
1034
+ ): Promise<CollectionTypeInfo['item']>
664
1035
  updateMany(args: {
665
- readonly data: readonly (UniqueWhereInput<ListTypeInfo> & {
666
- readonly data: ListTypeInfo['inputs']['update']
1036
+ readonly data: readonly (UniqueWhereInput<CollectionTypeInfo> & {
1037
+ readonly data: CollectionTypeInfo['inputs']['update']
667
1038
  })[]
668
- }): Promise<ListTypeInfo['item'][]>
1039
+ }): Promise<CollectionTypeInfo['item'][]>
669
1040
  createOne(args: {
670
- readonly data: ListTypeInfo['inputs']['create']
671
- }): Promise<ListTypeInfo['item']>
1041
+ readonly data: CollectionTypeInfo['inputs']['create']
1042
+ }): Promise<CollectionTypeInfo['item']>
672
1043
  createMany(args: {
673
- readonly data: readonly ListTypeInfo['inputs']['create'][]
674
- }): Promise<ListTypeInfo['item'][]>
675
- deleteOne(args: UniqueWhereInput<ListTypeInfo>): Promise<ListTypeInfo['item']>
1044
+ readonly data: readonly CollectionTypeInfo['inputs']['create'][]
1045
+ }): Promise<CollectionTypeInfo['item'][]>
1046
+ deleteOne(args: UniqueWhereInput<CollectionTypeInfo>): Promise<CollectionTypeInfo['item']>
676
1047
  deleteMany(args: {
677
- readonly where: readonly ListTypeInfo['inputs']['uniqueWhere'][]
678
- }): Promise<ListTypeInfo['item'][]>
1048
+ readonly where: readonly CollectionTypeInfo['inputs']['uniqueWhere'][]
1049
+ }): Promise<CollectionTypeInfo['item'][]>
679
1050
  }
680
1051
 
681
- export type NixxieDbAPI<ListsTypeInfo extends Record<string, BaseListTypeInfo>> = {
1052
+ export type NixxieDbAPI<ListsTypeInfo extends Record<string, BaseCollectionTypeInfo>> = {
682
1053
  [Key in keyof ListsTypeInfo]: DbAPI<ListsTypeInfo[Key]>
683
1054
  }
684
1055