@nixxie-cms/core 1.0.0 → 1.0.2

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 (187) hide show
  1. package/README.md +2 -2
  2. package/admin-ui/components/dist/nixxie-cms-core-admin-ui-components.cjs.js +4 -4
  3. package/admin-ui/components/dist/nixxie-cms-core-admin-ui-components.esm.js +4 -4
  4. package/admin-ui/context/dist/nixxie-cms-core-admin-ui-context.cjs.js +2 -2
  5. package/admin-ui/context/dist/nixxie-cms-core-admin-ui-context.esm.js +2 -2
  6. package/context/dist/nixxie-cms-core-context.cjs.js +2 -2
  7. package/context/dist/nixxie-cms-core-context.esm.js +2 -2
  8. package/dist/{CreateItemDialog-33335548.esm.js → CreateItemDialog-7008b050.esm.js} +1 -1
  9. package/dist/{CreateItemDialog-56cf59b7.cjs.js → CreateItemDialog-a0cab315.cjs.js} +1 -1
  10. package/dist/{PageContainer-7db73317.esm.js → PageContainer-5ae731cc.esm.js} +25 -18
  11. package/dist/{PageContainer-27c27f10.cjs.js → PageContainer-abd7159f.cjs.js} +25 -18
  12. package/dist/{admin-meta-graphql-6f7f5331.esm.js → admin-meta-graphql-0e6e606e.esm.js} +1 -1
  13. package/dist/{admin-meta-graphql-c8f926e9.cjs.js → admin-meta-graphql-306c224a.cjs.js} +1 -1
  14. package/dist/{context-3132c3ed.esm.js → context-af9957ed.esm.js} +2 -2
  15. package/dist/{context-e7a45152.cjs.js → context-b5204629.cjs.js} +2 -2
  16. package/dist/declarations/src/admin-ui/components/Navigation.d.ts.map +1 -1
  17. package/dist/declarations/src/admin-ui/components/PageContainer.d.ts.map +1 -1
  18. package/dist/declarations/src/helpers.d.ts.map +1 -1
  19. package/dist/declarations/src/index.d.ts +1 -0
  20. package/dist/declarations/src/index.d.ts.map +1 -1
  21. package/dist/declarations/src/internal-unstable/admin-ui/id-field-view.d.ts.map +1 -0
  22. package/dist/declarations/src/internal-unstable/admin-ui/pages/App/index.d.ts.map +1 -0
  23. package/dist/declarations/src/internal-unstable/admin-ui/pages/CreateItemPage/index.d.ts.map +1 -0
  24. package/dist/declarations/src/internal-unstable/admin-ui/pages/HomePage/index.d.ts.map +1 -0
  25. package/dist/declarations/src/internal-unstable/admin-ui/pages/ItemPage/index.d.ts.map +1 -0
  26. package/dist/declarations/src/internal-unstable/admin-ui/pages/ListPage/index.d.ts.map +1 -0
  27. package/dist/declarations/src/internal-unstable/admin-ui/pages/NoAccessPage/index.d.ts.map +1 -0
  28. package/dist/declarations/src/internal-unstable/artifacts.d.ts.map +1 -0
  29. package/dist/declarations/src/lib/core/initialise-lists.d.ts +1 -1
  30. package/dist/declarations/src/schema.d.ts.map +1 -1
  31. package/dist/declarations/src/types/config/index.d.ts +60 -1
  32. package/dist/declarations/src/types/config/index.d.ts.map +1 -1
  33. package/dist/declarations/src/types/config/lists.d.ts +4 -4
  34. package/dist/declarations/src/types/context.d.ts +150 -0
  35. package/dist/declarations/src/types/context.d.ts.map +1 -1
  36. package/dist/declarations/src/types/next-fields.d.ts +1 -1
  37. package/dist/{express-e9ed9a7d.cjs.js → express-455ae20c.cjs.js} +1 -1
  38. package/dist/{express-6743b918.esm.js → express-7559ca2d.esm.js} +1 -1
  39. package/dist/{index-ac01583b.cjs.js → index-89635494.cjs.js} +4 -4
  40. package/dist/{index-24b78415.esm.js → index-baa799e0.esm.js} +4 -4
  41. package/dist/nixxie-cms-core.cjs.js +104 -77
  42. package/dist/nixxie-cms-core.esm.js +104 -77
  43. package/dist/{non-null-graphql-5315718c.esm.js → non-null-graphql-a84ed64d.esm.js} +1 -1
  44. package/dist/{non-null-graphql-17b83ddc.cjs.js → non-null-graphql-add6bb3d.cjs.js} +1 -1
  45. package/dist/{resolve-hooks-66fe8a8e.cjs.js → resolve-hooks-165a9ce2.cjs.js} +1 -1
  46. package/dist/{resolve-hooks-17aafd37.esm.js → resolve-hooks-6813a045.esm.js} +2 -2
  47. package/dist/{system-dfec2f0a.esm.js → system-03e49e4f.esm.js} +8 -4
  48. package/dist/{system-48c5f6df.cjs.js → system-a321642d.cjs.js} +8 -4
  49. package/dist/{useFilter-0b5a1ee6.esm.js → useFilter-9b6db1f9.esm.js} +1 -1
  50. package/dist/{useFilter-1a4e6900.cjs.js → useFilter-acc9d413.cjs.js} +1 -1
  51. package/fields/dist/nixxie-cms-core-fields.cjs.js +16 -16
  52. package/fields/dist/nixxie-cms-core-fields.esm.js +17 -17
  53. package/fields/types/bytes/dist/nixxie-cms-core-fields-types-bytes.cjs.js +3 -3
  54. package/fields/types/bytes/dist/nixxie-cms-core-fields-types-bytes.esm.js +3 -3
  55. package/fields/types/bytes/views/dist/nixxie-cms-core-fields-types-bytes-views.cjs.js +1 -1
  56. package/fields/types/bytes/views/dist/nixxie-cms-core-fields-types-bytes-views.esm.js +1 -1
  57. package/fields/types/password/dist/nixxie-cms-core-fields-types-password.cjs.js +3 -3
  58. package/fields/types/password/dist/nixxie-cms-core-fields-types-password.esm.js +3 -3
  59. package/fields/types/relationship/views/dist/nixxie-cms-core-fields-types-relationship-views.cjs.js +4 -4
  60. package/fields/types/relationship/views/dist/nixxie-cms-core-fields-types-relationship-views.esm.js +4 -4
  61. package/fields/types/select/views/dist/nixxie-cms-core-fields-types-select-views.cjs.js +1 -1
  62. package/fields/types/select/views/dist/nixxie-cms-core-fields-types-select-views.esm.js +1 -1
  63. package/fields/types/text/views/dist/nixxie-cms-core-fields-types-text-views.cjs.js +1 -1
  64. package/fields/types/text/views/dist/nixxie-cms-core-fields-types-text-views.esm.js +1 -1
  65. package/internal-unstable/admin-ui/id-field-view/dist/nixxie-cms-core-internal-unstable-admin-ui-id-field-view.cjs.d.ts +2 -0
  66. package/internal-unstable/admin-ui/id-field-view/dist/nixxie-cms-core-internal-unstable-admin-ui-id-field-view.cjs.js +244 -0
  67. package/internal-unstable/admin-ui/id-field-view/dist/nixxie-cms-core-internal-unstable-admin-ui-id-field-view.esm.js +235 -0
  68. package/internal-unstable/admin-ui/id-field-view/package.json +4 -0
  69. package/internal-unstable/admin-ui/next-config/package.json +4 -0
  70. package/internal-unstable/admin-ui/pages/App/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-App.cjs.d.ts +2 -0
  71. package/internal-unstable/admin-ui/pages/App/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-App.cjs.js +59 -0
  72. package/internal-unstable/admin-ui/pages/App/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-App.esm.js +55 -0
  73. package/internal-unstable/admin-ui/pages/App/package.json +4 -0
  74. package/internal-unstable/admin-ui/pages/CreateItemPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-CreateItemPage.cjs.d.ts +2 -0
  75. package/internal-unstable/admin-ui/pages/CreateItemPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-CreateItemPage.cjs.js +116 -0
  76. package/internal-unstable/admin-ui/pages/CreateItemPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-CreateItemPage.esm.js +112 -0
  77. package/internal-unstable/admin-ui/pages/CreateItemPage/package.json +4 -0
  78. package/internal-unstable/admin-ui/pages/HomePage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-HomePage.cjs.d.ts +2 -0
  79. package/internal-unstable/admin-ui/pages/HomePage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-HomePage.cjs.js +336 -0
  80. package/internal-unstable/admin-ui/pages/HomePage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-HomePage.esm.js +332 -0
  81. package/internal-unstable/admin-ui/pages/HomePage/package.json +4 -0
  82. package/internal-unstable/admin-ui/pages/ItemPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-ItemPage.cjs.d.ts +2 -0
  83. package/internal-unstable/admin-ui/pages/ItemPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-ItemPage.cjs.js +463 -0
  84. package/internal-unstable/admin-ui/pages/ItemPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-ItemPage.esm.js +455 -0
  85. package/internal-unstable/admin-ui/pages/ItemPage/package.json +4 -0
  86. package/internal-unstable/admin-ui/pages/ListPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-ListPage.cjs.d.ts +2 -0
  87. package/internal-unstable/admin-ui/pages/ListPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-ListPage.cjs.js +1195 -0
  88. package/internal-unstable/admin-ui/pages/ListPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-ListPage.esm.js +1187 -0
  89. package/internal-unstable/admin-ui/pages/ListPage/package.json +4 -0
  90. package/internal-unstable/admin-ui/pages/NoAccessPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-NoAccessPage.cjs.d.ts +2 -0
  91. package/internal-unstable/admin-ui/pages/NoAccessPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-NoAccessPage.cjs.js +40 -0
  92. package/internal-unstable/admin-ui/pages/NoAccessPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-NoAccessPage.esm.js +35 -0
  93. package/internal-unstable/admin-ui/pages/NoAccessPage/package.json +4 -0
  94. package/internal-unstable/artifacts/dist/nixxie-cms-core-internal-unstable-artifacts.cjs.d.ts +2 -0
  95. package/internal-unstable/artifacts/dist/nixxie-cms-core-internal-unstable-artifacts.cjs.js +51 -0
  96. package/internal-unstable/artifacts/dist/nixxie-cms-core-internal-unstable-artifacts.esm.js +38 -0
  97. package/internal-unstable/artifacts/package.json +4 -0
  98. package/package.json +44 -44
  99. package/scripts/cli/dist/nixxie-cms-core-scripts-cli.cjs.js +44 -15
  100. package/scripts/cli/dist/nixxie-cms-core-scripts-cli.esm.js +44 -15
  101. package/scripts/dist/nixxie-cms-core-scripts.cjs.js +3 -3
  102. package/scripts/dist/nixxie-cms-core-scripts.esm.js +3 -3
  103. package/src/admin-ui/admin-meta-graphql.ts +168 -168
  104. package/src/admin-ui/components/CommandPalette.tsx +433 -431
  105. package/src/admin-ui/components/Navigation.tsx +389 -385
  106. package/src/admin-ui/components/PageContainer.tsx +311 -310
  107. package/src/admin-ui/components/WelcomeDialog.tsx +1 -1
  108. package/src/admin-ui/context.tsx +338 -338
  109. package/src/admin-ui/templates/app.ts +60 -60
  110. package/src/admin-ui/templates/create-item.ts +5 -5
  111. package/src/admin-ui/templates/home.ts +2 -2
  112. package/src/admin-ui/templates/item.tsx +5 -5
  113. package/src/admin-ui/templates/list.tsx +5 -5
  114. package/src/admin-ui/templates/next-config.ts +29 -0
  115. package/src/admin-ui/templates/no-access.ts +7 -7
  116. package/src/fields/types/bigInt/index.ts +181 -181
  117. package/src/fields/types/bytes/index.ts +275 -275
  118. package/src/fields/types/calendarDay/index.ts +194 -194
  119. package/src/fields/types/checkbox/index.ts +76 -76
  120. package/src/fields/types/decimal/index.ts +182 -182
  121. package/src/fields/types/file/index.ts +168 -168
  122. package/src/fields/types/float/index.ts +133 -133
  123. package/src/fields/types/image/index.ts +244 -244
  124. package/src/fields/types/integer/index.ts +156 -156
  125. package/src/fields/types/json/index.ts +77 -77
  126. package/src/fields/types/multiselect/index.ts +212 -212
  127. package/src/fields/types/password/index.ts +241 -241
  128. package/src/fields/types/relationship/index.ts +381 -381
  129. package/src/fields/types/relationship/views/RelationshipTable.tsx +190 -190
  130. package/src/fields/types/select/index.ts +226 -226
  131. package/src/fields/types/text/index.ts +207 -207
  132. package/src/fields/types/timestamp/index.ts +116 -116
  133. package/src/fields/types/virtual/index.ts +108 -108
  134. package/src/helpers.ts +342 -316
  135. package/src/index.ts +4 -0
  136. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/id-field-view.tsx +167 -167
  137. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/App/index.tsx +22 -22
  138. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/CreateItemPage/index.tsx +71 -71
  139. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/HomePage/index.tsx +333 -333
  140. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/ItemPage/common.tsx +358 -358
  141. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/ItemPage/index.tsx +483 -483
  142. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/ListPage/FilterAdd.tsx +221 -221
  143. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/ListPage/PaginationControls.tsx +170 -170
  144. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/ListPage/Tag.tsx +72 -72
  145. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/ListPage/index.tsx +1006 -1006
  146. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/NoAccessPage/index.tsx +24 -24
  147. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/artifacts.ts +5 -5
  148. package/src/lib/context/createContext.ts +165 -161
  149. package/src/lib/core/initialise-lists.ts +1097 -1097
  150. package/src/lib/id-field.ts +214 -214
  151. package/src/lib/telemetry.ts +342 -342
  152. package/src/schema.ts +237 -233
  153. package/src/scripts/telemetry.ts +1 -1
  154. package/src/types/config/index.ts +400 -333
  155. package/src/types/config/lists.ts +4 -4
  156. package/src/types/context.ts +700 -530
  157. package/src/types/next-fields.ts +499 -499
  158. package/src/types/telemetry.ts +51 -51
  159. package/tests/telemetry.test.ts +361 -361
  160. package/CHANGELOG.md +0 -3158
  161. package/___internal-do-not-use-will-break-in-patch/admin-ui/id-field-view/package.json +0 -4
  162. package/___internal-do-not-use-will-break-in-patch/admin-ui/next-config/package.json +0 -4
  163. package/___internal-do-not-use-will-break-in-patch/admin-ui/pages/App/package.json +0 -4
  164. package/___internal-do-not-use-will-break-in-patch/admin-ui/pages/CreateItemPage/package.json +0 -4
  165. package/___internal-do-not-use-will-break-in-patch/admin-ui/pages/HomePage/package.json +0 -4
  166. package/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ItemPage/package.json +0 -4
  167. package/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ListPage/package.json +0 -4
  168. package/___internal-do-not-use-will-break-in-patch/admin-ui/pages/NoAccessPage/package.json +0 -4
  169. package/___internal-do-not-use-will-break-in-patch/artifacts/package.json +0 -4
  170. package/dist/declarations/src/___internal-do-not-use-will-break-in-patch/admin-ui/id-field-view.d.ts.map +0 -1
  171. package/dist/declarations/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/App/index.d.ts.map +0 -1
  172. package/dist/declarations/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/CreateItemPage/index.d.ts.map +0 -1
  173. package/dist/declarations/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/HomePage/index.d.ts.map +0 -1
  174. package/dist/declarations/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ItemPage/index.d.ts.map +0 -1
  175. package/dist/declarations/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ListPage/index.d.ts.map +0 -1
  176. package/dist/declarations/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/NoAccessPage/index.d.ts.map +0 -1
  177. package/dist/declarations/src/___internal-do-not-use-will-break-in-patch/artifacts.d.ts.map +0 -1
  178. /package/dist/{common-1a350e11.cjs.js → common-5933f758.cjs.js} +0 -0
  179. /package/dist/{common-29fc82e6.esm.js → common-ea5c441a.esm.js} +0 -0
  180. /package/dist/declarations/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/id-field-view.d.ts +0 -0
  181. /package/dist/declarations/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/App/index.d.ts +0 -0
  182. /package/dist/declarations/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/CreateItemPage/index.d.ts +0 -0
  183. /package/dist/declarations/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/HomePage/index.d.ts +0 -0
  184. /package/dist/declarations/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/ItemPage/index.d.ts +0 -0
  185. /package/dist/declarations/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/ListPage/index.d.ts +0 -0
  186. /package/dist/declarations/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/NoAccessPage/index.d.ts +0 -0
  187. /package/dist/declarations/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/artifacts.d.ts +0 -0
package/src/helpers.ts CHANGED
@@ -1,316 +1,342 @@
1
- import { list, config, action } from './schema'
2
- import { timestamp } from './fields/types/timestamp'
3
- import { relationship } from './fields/types/relationship'
4
- import type {
5
- BaseListTypeInfo,
6
- BaseNixxieTypeInfo,
7
- ListConfig,
8
- BaseFields,
9
- NixxieConfigPre,
10
- NixxieConfig,
11
- ListHooks,
12
- ActionArgsConfig,
13
- Action,
14
- DeclaredAction,
15
- } from './types'
16
-
17
- // ================================================================
18
- // Validators — use inside field `validation` config
19
- // ================================================================
20
-
21
- export const validators = {
22
- /** Validates a string is a well-formed email address */
23
- email: {
24
- match: {
25
- regex: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
26
- explanation: 'Must be a valid email address',
27
- },
28
- },
29
-
30
- /** Validates a string starts with http:// or https:// */
31
- url: {
32
- match: {
33
- regex: /^https?:\/\/.+/,
34
- explanation: 'Must be a valid URL starting with http:// or https://',
35
- },
36
- },
37
-
38
- /** Validates a string has at least `min` characters */
39
- minLength: (min: number) => ({
40
- length: { min } as const,
41
- }),
42
-
43
- /** Validates a string has at most `max` characters */
44
- maxLength: (max: number) => ({
45
- length: { max } as const,
46
- }),
47
-
48
- /** Validates a string has between `min` and `max` characters */
49
- length: (min: number, max: number) => ({
50
- length: { min, max } as const,
51
- }),
52
-
53
- /** Validates a string matches the given regex pattern */
54
- regex: (regex: RegExp, explanation?: string) => ({
55
- match: {
56
- regex,
57
- explanation: explanation ?? `Must match pattern ${regex}`,
58
- },
59
- }),
60
-
61
- /** Marks a field as required */
62
- required: { isRequired: true } as const,
63
- } as const
64
-
65
- // ================================================================
66
- // Mixin system
67
- // ================================================================
68
-
69
- export type ListMixin<ListTypeInfo extends BaseListTypeInfo = BaseListTypeInfo> = {
70
- /** Fields added by this mixin user fields take precedence on key conflicts */
71
- fields?: Record<string, any>
72
- /** Hooks merged with existing list hooks */
73
- hooks?: Partial<ListHooks<ListTypeInfo>>
74
- }
75
-
76
- /**
77
- * Adds `createdAt` and `updatedAt` timestamp fields.
78
- * Both are hidden on create forms and read-only in item views.
79
- */
80
- export function withTimestamps(): ListMixin {
81
- return {
82
- fields: {
83
- createdAt: timestamp({
84
- defaultValue: { kind: 'now' },
85
- db: { isNullable: false },
86
- ui: {
87
- createView: { fieldMode: 'hidden' },
88
- itemView: { fieldMode: 'read' },
89
- listView: { fieldMode: 'read' },
90
- },
91
- }),
92
- updatedAt: timestamp({
93
- db: { updatedAt: true, isNullable: false },
94
- ui: {
95
- createView: { fieldMode: 'hidden' },
96
- itemView: { fieldMode: 'read' },
97
- listView: { fieldMode: 'read' },
98
- },
99
- }),
100
- },
101
- }
102
- }
103
-
104
- /**
105
- * Adds a `deletedAt` field for soft-delete support.
106
- * Items are not physically deleted — set `deletedAt` to the current time instead.
107
- * Note: you must manually filter `deletedAt: null` in your queries/access control.
108
- */
109
- export function withSoftDelete(): ListMixin {
110
- return {
111
- fields: {
112
- deletedAt: timestamp({
113
- db: { isNullable: true },
114
- ui: {
115
- createView: { fieldMode: 'hidden' },
116
- itemView: { fieldMode: 'read' },
117
- listView: { fieldMode: 'read' },
118
- },
119
- }),
120
- },
121
- }
122
- }
123
-
124
- /**
125
- * Adds `createdBy` and `updatedBy` relationship fields that automatically
126
- * track which user performed each write operation.
127
- *
128
- * Requires session to have an `itemId` (standard Nixxie auth).
129
- *
130
- * @param userListKey - The list key for your user model (default: 'User')
131
- */
132
- export function withAudit(userListKey = 'User'): ListMixin {
133
- return {
134
- fields: {
135
- createdBy: relationship({
136
- ref: userListKey,
137
- ui: {
138
- createView: { fieldMode: 'hidden' },
139
- itemView: { fieldMode: 'read' },
140
- },
141
- }),
142
- updatedBy: relationship({
143
- ref: userListKey,
144
- ui: {
145
- createView: { fieldMode: 'hidden' },
146
- itemView: { fieldMode: 'read' },
147
- },
148
- }),
149
- },
150
- hooks: {
151
- resolveInput: async ({ operation, resolvedData, context }) => {
152
- const userId = (context as any).session?.itemId
153
- if (!userId) return resolvedData
154
- if (operation === 'create') {
155
- return {
156
- ...resolvedData,
157
- createdBy: { connect: { id: userId } },
158
- updatedBy: { connect: { id: userId } },
159
- }
160
- }
161
- if (operation === 'update') {
162
- return {
163
- ...resolvedData,
164
- updatedBy: { connect: { id: userId } },
165
- }
166
- }
167
- return resolvedData
168
- },
169
- },
170
- }
171
- }
172
-
173
- /**
174
- * Factory for building reusable field group mixins.
175
- *
176
- * @example
177
- * const withAddress = createMixin({
178
- * street: text(),
179
- * city: text(),
180
- * postcode: text({ validation: validators.regex(/^\d{4}$/, 'Must be a 4-digit postcode') }),
181
- * })
182
- */
183
- export function createMixin(
184
- fields: Record<string, any>,
185
- hooks?: Partial<ListHooks<any>>
186
- ): ListMixin {
187
- return { fields, hooks }
188
- }
189
-
190
- // ================================================================
191
- // defineList — a typed wrapper around list() with mixin support
192
- // ================================================================
193
-
194
- type DefineListConfig<ListTypeInfo extends BaseListTypeInfo> = ListConfig<ListTypeInfo> & {
195
- /**
196
- * Mixins to apply to this list. Mixin fields are merged before user fields,
197
- * so user fields always take precedence on key conflicts.
198
- */
199
- mixins?: ListMixin[]
200
- }
201
-
202
- /**
203
- * Defines a list with optional mixins for reusable field groups and hooks.
204
- *
205
- * @example
206
- * export const Post = defineList({
207
- * mixins: [withTimestamps()],
208
- * fields: {
209
- * title: text({ validation: validators.required }),
210
- * content: text(),
211
- * },
212
- * })
213
- */
214
- export function defineList<ListTypeInfo extends BaseListTypeInfo>(
215
- config: DefineListConfig<ListTypeInfo>
216
- ): ListConfig<ListTypeInfo> {
217
- const { mixins = [], ...listConfig } = config
218
-
219
- const mixinFields: Record<string, any> = {}
220
- const mixinHooksList: Array<Partial<ListHooks<any>>> = []
221
-
222
- for (const mixin of mixins) {
223
- if (mixin.fields) Object.assign(mixinFields, mixin.fields)
224
- if (mixin.hooks) mixinHooksList.push(mixin.hooks)
225
- }
226
-
227
- const mergedHooks = mergeMixinHooks(mixinHooksList, listConfig.hooks as Partial<ListHooks<any>>)
228
-
229
- return list({
230
- ...listConfig,
231
- fields: {
232
- ...mixinFields,
233
- ...listConfig.fields,
234
- } as BaseFields<ListTypeInfo>,
235
- hooks: mergedHooks as ListConfig<ListTypeInfo>['hooks'],
236
- } as ListConfig<ListTypeInfo>)
237
- }
238
-
239
- function mergeMixinHooks(
240
- mixinHooks: Array<Partial<ListHooks<any>>>,
241
- listHooks: Partial<ListHooks<any>> | undefined
242
- ): Partial<ListHooks<any>> {
243
- if (mixinHooks.length === 0) return listHooks ?? {}
244
-
245
- const hookNames: Array<keyof ListHooks<any>> = [
246
- 'resolveInput',
247
- 'validate',
248
- 'beforeOperation',
249
- 'afterOperation',
250
- ]
251
- const merged: Partial<ListHooks<any>> = {}
252
-
253
- for (const hookName of hookNames) {
254
- const fns = [
255
- ...mixinHooks.map(h => h[hookName]).filter(Boolean),
256
- ...(listHooks?.[hookName] ? [listHooks[hookName]] : []),
257
- ] as ((...args: any[]) => any)[]
258
-
259
- if (fns.length === 0) continue
260
- if (fns.length === 1) {
261
- ;(merged as any)[hookName] = fns[0]
262
- continue
263
- }
264
- // Run all hooks in sequence; for resolveInput, pass the output of each as input to the next
265
- if (hookName === 'resolveInput') {
266
- ;(merged as any)[hookName] = async (args: any) => {
267
- let result = args.resolvedData
268
- for (const fn of fns) {
269
- const out = await fn({ ...args, resolvedData: result })
270
- if (out !== undefined) result = out
271
- }
272
- return result
273
- }
274
- } else {
275
- ;(merged as any)[hookName] = async (args: any) => {
276
- for (const fn of fns) await fn(args)
277
- }
278
- }
279
- }
280
-
281
- return merged
282
- }
283
-
284
- // ================================================================
285
- // defineConfig — typed alias for config()
286
- // ================================================================
287
-
288
- /**
289
- * Defines the Nixxie configuration. This is a typed alias for `config()` that
290
- * provides the same functionality with a more declarative name.
291
- *
292
- * @example
293
- * export default defineConfig({
294
- * db: { provider: 'sqlite', url: 'file:./nixxie.db' },
295
- * lists: { Post, User },
296
- * })
297
- */
298
- export function defineConfig<TypeInfo extends BaseNixxieTypeInfo>(
299
- nixxieConfig: NixxieConfigPre<TypeInfo>
300
- ): NixxieConfig<TypeInfo> {
301
- return config(nixxieConfig)
302
- }
303
-
304
- // ================================================================
305
- // defineAction — typed alias for action()
306
- // ================================================================
307
-
308
- /**
309
- * Defines a list action. Typed alias for `action()`.
310
- */
311
- export function defineAction<
312
- ListTypeInfo extends BaseListTypeInfo,
313
- Args extends ActionArgsConfig<ListTypeInfo> | undefined = undefined,
314
- >(actionConfig: Action<ListTypeInfo, Args>): DeclaredAction<ListTypeInfo> {
315
- return action(actionConfig)
316
- }
1
+ import { list, config, action } from './schema'
2
+ import { timestamp } from './fields/types/timestamp'
3
+ import { relationship } from './fields/types/relationship'
4
+ import { merge } from './fields/resolve-hooks'
5
+ import type {
6
+ BaseListTypeInfo,
7
+ BaseNixxieTypeInfo,
8
+ ListConfig,
9
+ BaseFields,
10
+ NixxieConfigPre,
11
+ NixxieConfig,
12
+ ListHooks,
13
+ ActionArgsConfig,
14
+ Action,
15
+ DeclaredAction,
16
+ } from './types'
17
+
18
+ // ================================================================
19
+ // Validators — use inside field `validation` config
20
+ // ================================================================
21
+
22
+ export const validators = {
23
+ /** Validates a string is a well-formed email address */
24
+ email: {
25
+ match: {
26
+ regex: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
27
+ explanation: 'Must be a valid email address',
28
+ },
29
+ },
30
+
31
+ /** Validates a string starts with http:// or https:// */
32
+ url: {
33
+ match: {
34
+ regex: /^https?:\/\/.+/,
35
+ explanation: 'Must be a valid URL starting with http:// or https://',
36
+ },
37
+ },
38
+
39
+ /** Validates a string has at least `min` characters */
40
+ minLength: (min: number) => ({
41
+ length: { min } as const,
42
+ }),
43
+
44
+ /** Validates a string has at most `max` characters */
45
+ maxLength: (max: number) => ({
46
+ length: { max } as const,
47
+ }),
48
+
49
+ /** Validates a string has between `min` and `max` characters */
50
+ length: (min: number, max: number) => ({
51
+ length: { min, max } as const,
52
+ }),
53
+
54
+ /** Validates a string matches the given regex pattern */
55
+ regex: (regex: RegExp, explanation?: string) => ({
56
+ match: {
57
+ regex,
58
+ explanation: explanation ?? `Must match pattern ${regex}`,
59
+ },
60
+ }),
61
+
62
+ /** Marks a field as required */
63
+ required: { isRequired: true } as const,
64
+ } as const
65
+
66
+ // ================================================================
67
+ // Mixin system
68
+ // ================================================================
69
+
70
+ export type ListMixin<ListTypeInfo extends BaseListTypeInfo = BaseListTypeInfo> = {
71
+ /** Fields added by this mixin — user fields take precedence on key conflicts */
72
+ fields?: Record<string, any>
73
+ /** Hooks merged with existing list hooks */
74
+ hooks?: Partial<ListHooks<ListTypeInfo>>
75
+ }
76
+
77
+ /**
78
+ * Adds `createdAt` and `updatedAt` timestamp fields.
79
+ * Both are hidden on create forms and read-only in item views.
80
+ */
81
+ export function withTimestamps(): ListMixin {
82
+ return {
83
+ fields: {
84
+ createdAt: timestamp({
85
+ defaultValue: { kind: 'now' },
86
+ db: { isNullable: false },
87
+ ui: {
88
+ createView: { fieldMode: 'hidden' },
89
+ itemView: { fieldMode: 'read' },
90
+ listView: { fieldMode: 'read' },
91
+ },
92
+ }),
93
+ updatedAt: timestamp({
94
+ db: { updatedAt: true, isNullable: false },
95
+ ui: {
96
+ createView: { fieldMode: 'hidden' },
97
+ itemView: { fieldMode: 'read' },
98
+ listView: { fieldMode: 'read' },
99
+ },
100
+ }),
101
+ },
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Adds a `deletedAt` field for soft-delete support.
107
+ * Items are not physically deleted — set `deletedAt` to the current time instead.
108
+ * Note: you must manually filter `deletedAt: null` in your queries/access control.
109
+ */
110
+ export function withSoftDelete(): ListMixin {
111
+ return {
112
+ fields: {
113
+ deletedAt: timestamp({
114
+ db: { isNullable: true },
115
+ ui: {
116
+ createView: { fieldMode: 'hidden' },
117
+ itemView: { fieldMode: 'read' },
118
+ listView: { fieldMode: 'read' },
119
+ },
120
+ }),
121
+ },
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Adds `createdBy` and `updatedBy` relationship fields that automatically
127
+ * track which user performed each write operation.
128
+ *
129
+ * Requires session to have an `itemId` (standard Nixxie auth).
130
+ *
131
+ * @param userListKey - The list key for your user model (default: 'User')
132
+ */
133
+ export function withAudit(userListKey = 'User'): ListMixin {
134
+ return {
135
+ fields: {
136
+ createdBy: relationship({
137
+ ref: userListKey,
138
+ ui: {
139
+ createView: { fieldMode: 'hidden' },
140
+ itemView: { fieldMode: 'read' },
141
+ },
142
+ }),
143
+ updatedBy: relationship({
144
+ ref: userListKey,
145
+ ui: {
146
+ createView: { fieldMode: 'hidden' },
147
+ itemView: { fieldMode: 'read' },
148
+ },
149
+ }),
150
+ },
151
+ hooks: {
152
+ resolveInput: async ({ operation, resolvedData, context }) => {
153
+ const userId = (context as any).session?.itemId
154
+ if (!userId) return resolvedData
155
+ if (operation === 'create') {
156
+ return {
157
+ ...resolvedData,
158
+ createdBy: resolvedData.createdBy ?? { connect: { id: userId } },
159
+ updatedBy: resolvedData.updatedBy ?? { connect: { id: userId } },
160
+ }
161
+ }
162
+ if (operation === 'update') {
163
+ return {
164
+ ...resolvedData,
165
+ updatedBy: resolvedData.updatedBy ?? { connect: { id: userId } },
166
+ }
167
+ }
168
+ return resolvedData
169
+ },
170
+ },
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Factory for building reusable field group mixins.
176
+ *
177
+ * @example
178
+ * const withAddress = createMixin({
179
+ * street: text(),
180
+ * city: text(),
181
+ * postcode: text({ validation: validators.regex(/^\d{4}$/, 'Must be a 4-digit postcode') }),
182
+ * })
183
+ */
184
+ export function createMixin(
185
+ fields: Record<string, any>,
186
+ hooks?: Partial<ListHooks<any>>
187
+ ): ListMixin {
188
+ return { fields, hooks }
189
+ }
190
+
191
+ // ================================================================
192
+ // defineList — a typed wrapper around list() with mixin support
193
+ // ================================================================
194
+
195
+ type DefineListConfig<ListTypeInfo extends BaseListTypeInfo> = ListConfig<ListTypeInfo> & {
196
+ /**
197
+ * Mixins to apply to this list. Mixin fields are merged before user fields,
198
+ * so user fields always take precedence on key conflicts.
199
+ */
200
+ mixins?: ListMixin[]
201
+ }
202
+
203
+ /**
204
+ * Defines a list with optional mixins for reusable field groups and hooks.
205
+ *
206
+ * @example
207
+ * export const Post = defineList({
208
+ * mixins: [withTimestamps()],
209
+ * fields: {
210
+ * title: text({ validation: validators.required }),
211
+ * content: text(),
212
+ * },
213
+ * })
214
+ */
215
+ export function defineList<ListTypeInfo extends BaseListTypeInfo>(
216
+ config: DefineListConfig<ListTypeInfo>
217
+ ): ListConfig<ListTypeInfo> {
218
+ const { mixins = [], ...listConfig } = config
219
+
220
+ const mixinFields: Record<string, any> = {}
221
+ const mixinHooksList: Array<Partial<ListHooks<any>>> = []
222
+
223
+ for (const mixin of mixins) {
224
+ if (mixin.fields) Object.assign(mixinFields, mixin.fields)
225
+ if (mixin.hooks) mixinHooksList.push(mixin.hooks)
226
+ }
227
+
228
+ const mergedHooks = mergeMixinHooks(mixinHooksList, listConfig.hooks as Partial<ListHooks<any>>)
229
+
230
+ return list({
231
+ ...listConfig,
232
+ fields: {
233
+ ...mixinFields,
234
+ ...listConfig.fields,
235
+ } as BaseFields<ListTypeInfo>,
236
+ hooks: mergedHooks as ListConfig<ListTypeInfo>['hooks'],
237
+ } as ListConfig<ListTypeInfo>)
238
+ }
239
+
240
+ type AnyResolveInput =
241
+ | ((args: any) => any)
242
+ | { create?: (args: any) => any; update?: (args: any) => any }
243
+
244
+ function expandResolveInput(hook: AnyResolveInput) {
245
+ return typeof hook === 'function' ? { create: hook, update: hook } : hook
246
+ }
247
+
248
+ /**
249
+ * Compose two `resolveInput` hooks so the second runs on the `resolvedData` produced by the first.
250
+ * Handles both the function form and the `{ create, update }` object form, and treats a hook that
251
+ * returns `undefined` as "no change" (keeps the prior resolvedData) so chained mixins don't wipe data.
252
+ */
253
+ function mergeResolveInput(
254
+ a: AnyResolveInput | undefined,
255
+ b: AnyResolveInput | undefined
256
+ ): AnyResolveInput | undefined {
257
+ if (!a) return b
258
+ if (!b) return a
259
+ const ea = expandResolveInput(a)
260
+ const eb = expandResolveInput(b)
261
+ const chain = (f?: (args: any) => any, g?: (args: any) => any) => {
262
+ if (!f) return g
263
+ if (!g) return f
264
+ return async (args: any) => {
265
+ const first = await f(args)
266
+ const resolvedData = first === undefined ? args.resolvedData : first
267
+ const second = await g({ ...args, resolvedData })
268
+ return second === undefined ? resolvedData : second
269
+ }
270
+ }
271
+ return {
272
+ create: chain(ea.create, eb.create),
273
+ update: chain(ea.update, eb.update),
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Merge mixin hooks with the list's own hooks. Each Keystone hook may be either a function or a
279
+ * `{ create, update, delete }` object — `merge` (from resolve-hooks) handles both forms for the
280
+ * void hooks, and `mergeResolveInput` handles the data-threading `resolveInput` hook.
281
+ */
282
+ function mergeMixinHooks(
283
+ mixinHooks: Array<Partial<ListHooks<any>>>,
284
+ listHooks: Partial<ListHooks<any>> | undefined
285
+ ): Partial<ListHooks<any>> {
286
+ if (mixinHooks.length === 0) return listHooks ?? {}
287
+
288
+ const all = [...mixinHooks, ...(listHooks ? [listHooks] : [])]
289
+ const merged: Partial<ListHooks<any>> = {}
290
+
291
+ for (const hooks of all) {
292
+ merged.resolveInput = mergeResolveInput(
293
+ merged.resolveInput as AnyResolveInput | undefined,
294
+ hooks.resolveInput as AnyResolveInput | undefined
295
+ ) as ListHooks<any>['resolveInput']
296
+ merged.validate = merge(merged.validate as any, hooks.validate as any) as ListHooks<any>['validate']
297
+ merged.beforeOperation = merge(
298
+ merged.beforeOperation as any,
299
+ hooks.beforeOperation as any
300
+ ) as ListHooks<any>['beforeOperation']
301
+ merged.afterOperation = merge(
302
+ merged.afterOperation as any,
303
+ hooks.afterOperation as any
304
+ ) as ListHooks<any>['afterOperation']
305
+ }
306
+
307
+ return merged
308
+ }
309
+
310
+ // ================================================================
311
+ // defineConfig — typed alias for config()
312
+ // ================================================================
313
+
314
+ /**
315
+ * Defines the Nixxie configuration. This is a typed alias for `config()` that
316
+ * provides the same functionality with a more declarative name.
317
+ *
318
+ * @example
319
+ * export default defineConfig({
320
+ * db: { provider: 'sqlite', url: 'file:./nixxie.db' },
321
+ * lists: { Post, User },
322
+ * })
323
+ */
324
+ export function defineConfig<TypeInfo extends BaseNixxieTypeInfo>(
325
+ nixxieConfig: NixxieConfigPre<TypeInfo>
326
+ ): NixxieConfig<TypeInfo> {
327
+ return config(nixxieConfig)
328
+ }
329
+
330
+ // ================================================================
331
+ // defineAction — typed alias for action()
332
+ // ================================================================
333
+
334
+ /**
335
+ * Defines a list action. Typed alias for `action()`.
336
+ */
337
+ export function defineAction<
338
+ ListTypeInfo extends BaseListTypeInfo,
339
+ Args extends ActionArgsConfig<ListTypeInfo> | undefined = undefined,
340
+ >(actionConfig: Action<ListTypeInfo, Args>): DeclaredAction<ListTypeInfo> {
341
+ return action(actionConfig)
342
+ }