@nixxie-cms/core 1.0.0 → 1.0.1

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 (186) 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 +15 -15
  100. package/scripts/cli/dist/nixxie-cms-core-scripts-cli.esm.js +15 -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/no-access.ts +7 -7
  115. package/src/fields/types/bigInt/index.ts +181 -181
  116. package/src/fields/types/bytes/index.ts +275 -275
  117. package/src/fields/types/calendarDay/index.ts +194 -194
  118. package/src/fields/types/checkbox/index.ts +76 -76
  119. package/src/fields/types/decimal/index.ts +182 -182
  120. package/src/fields/types/file/index.ts +168 -168
  121. package/src/fields/types/float/index.ts +133 -133
  122. package/src/fields/types/image/index.ts +244 -244
  123. package/src/fields/types/integer/index.ts +156 -156
  124. package/src/fields/types/json/index.ts +77 -77
  125. package/src/fields/types/multiselect/index.ts +212 -212
  126. package/src/fields/types/password/index.ts +241 -241
  127. package/src/fields/types/relationship/index.ts +381 -381
  128. package/src/fields/types/relationship/views/RelationshipTable.tsx +190 -190
  129. package/src/fields/types/select/index.ts +226 -226
  130. package/src/fields/types/text/index.ts +207 -207
  131. package/src/fields/types/timestamp/index.ts +116 -116
  132. package/src/fields/types/virtual/index.ts +108 -108
  133. package/src/helpers.ts +342 -316
  134. package/src/index.ts +4 -0
  135. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/id-field-view.tsx +167 -167
  136. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/App/index.tsx +22 -22
  137. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/CreateItemPage/index.tsx +71 -71
  138. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/HomePage/index.tsx +333 -333
  139. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/ItemPage/common.tsx +358 -358
  140. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/ItemPage/index.tsx +483 -483
  141. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/ListPage/FilterAdd.tsx +221 -221
  142. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/ListPage/PaginationControls.tsx +170 -170
  143. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/ListPage/Tag.tsx +72 -72
  144. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/ListPage/index.tsx +1006 -1006
  145. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/NoAccessPage/index.tsx +24 -24
  146. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/artifacts.ts +5 -5
  147. package/src/lib/context/createContext.ts +165 -161
  148. package/src/lib/core/initialise-lists.ts +1097 -1097
  149. package/src/lib/id-field.ts +214 -214
  150. package/src/lib/telemetry.ts +342 -342
  151. package/src/schema.ts +237 -233
  152. package/src/scripts/telemetry.ts +1 -1
  153. package/src/types/config/index.ts +400 -333
  154. package/src/types/config/lists.ts +4 -4
  155. package/src/types/context.ts +700 -530
  156. package/src/types/next-fields.ts +499 -499
  157. package/src/types/telemetry.ts +51 -51
  158. package/tests/telemetry.test.ts +361 -361
  159. package/CHANGELOG.md +0 -3158
  160. package/___internal-do-not-use-will-break-in-patch/admin-ui/id-field-view/package.json +0 -4
  161. package/___internal-do-not-use-will-break-in-patch/admin-ui/next-config/package.json +0 -4
  162. package/___internal-do-not-use-will-break-in-patch/admin-ui/pages/App/package.json +0 -4
  163. package/___internal-do-not-use-will-break-in-patch/admin-ui/pages/CreateItemPage/package.json +0 -4
  164. package/___internal-do-not-use-will-break-in-patch/admin-ui/pages/HomePage/package.json +0 -4
  165. package/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ItemPage/package.json +0 -4
  166. package/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ListPage/package.json +0 -4
  167. package/___internal-do-not-use-will-break-in-patch/admin-ui/pages/NoAccessPage/package.json +0 -4
  168. package/___internal-do-not-use-will-break-in-patch/artifacts/package.json +0 -4
  169. package/dist/declarations/src/___internal-do-not-use-will-break-in-patch/admin-ui/id-field-view.d.ts.map +0 -1
  170. package/dist/declarations/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/App/index.d.ts.map +0 -1
  171. package/dist/declarations/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/CreateItemPage/index.d.ts.map +0 -1
  172. package/dist/declarations/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/HomePage/index.d.ts.map +0 -1
  173. package/dist/declarations/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ItemPage/index.d.ts.map +0 -1
  174. package/dist/declarations/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ListPage/index.d.ts.map +0 -1
  175. package/dist/declarations/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/NoAccessPage/index.d.ts.map +0 -1
  176. package/dist/declarations/src/___internal-do-not-use-will-break-in-patch/artifacts.d.ts.map +0 -1
  177. /package/dist/{common-1a350e11.cjs.js → common-5933f758.cjs.js} +0 -0
  178. /package/dist/{common-29fc82e6.esm.js → common-ea5c441a.esm.js} +0 -0
  179. /package/dist/declarations/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/id-field-view.d.ts +0 -0
  180. /package/dist/declarations/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/App/index.d.ts +0 -0
  181. /package/dist/declarations/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/CreateItemPage/index.d.ts +0 -0
  182. /package/dist/declarations/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/HomePage/index.d.ts +0 -0
  183. /package/dist/declarations/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/ItemPage/index.d.ts +0 -0
  184. /package/dist/declarations/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/ListPage/index.d.ts +0 -0
  185. /package/dist/declarations/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/NoAccessPage/index.d.ts +0 -0
  186. /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
+ }