@nixxie-cms/core 1.0.3 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/CHANGES-1.1.md +134 -0
- package/context/dist/nixxie-cms-core-context.cjs.js +4 -3
- package/context/dist/nixxie-cms-core-context.esm.js +3 -2
- package/dist/declarations/src/access.d.ts +2 -2
- package/dist/declarations/src/access.d.ts.map +1 -1
- package/dist/declarations/src/admin-ui/components/Navigation.d.ts +2 -2
- package/dist/declarations/src/admin-ui/components/Navigation.d.ts.map +1 -1
- package/dist/declarations/src/admin-ui/context.d.ts +6 -6
- package/dist/declarations/src/admin-ui/context.d.ts.map +1 -1
- package/dist/declarations/src/admin-ui/utils/Fields.d.ts +3 -3
- package/dist/declarations/src/admin-ui/utils/Fields.d.ts.map +1 -1
- package/dist/declarations/src/admin-ui/utils/filters.d.ts +5 -5
- package/dist/declarations/src/admin-ui/utils/filters.d.ts.map +1 -1
- package/dist/declarations/src/admin-ui/utils/useCreateItem.d.ts +3 -3
- package/dist/declarations/src/admin-ui/utils/useCreateItem.d.ts.map +1 -1
- package/dist/declarations/src/admin-ui/utils/utils.d.ts +2 -2
- package/dist/declarations/src/admin-ui/utils/utils.d.ts.map +1 -1
- package/dist/declarations/src/context.d.ts +1 -1
- package/dist/declarations/src/context.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/bigInt/index.d.ts +3 -3
- package/dist/declarations/src/fields/types/bigInt/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/bytes/index.d.ts +3 -3
- package/dist/declarations/src/fields/types/bytes/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/calendarDay/index.d.ts +3 -3
- package/dist/declarations/src/fields/types/calendarDay/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/checkbox/index.d.ts +3 -3
- package/dist/declarations/src/fields/types/checkbox/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/decimal/index.d.ts +3 -3
- package/dist/declarations/src/fields/types/decimal/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/file/index.d.ts +4 -4
- package/dist/declarations/src/fields/types/file/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/float/index.d.ts +3 -3
- package/dist/declarations/src/fields/types/float/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/image/index.d.ts +4 -4
- package/dist/declarations/src/fields/types/image/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/integer/index.d.ts +3 -3
- package/dist/declarations/src/fields/types/integer/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/json/index.d.ts +3 -3
- package/dist/declarations/src/fields/types/json/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/multiselect/index.d.ts +3 -3
- package/dist/declarations/src/fields/types/multiselect/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/multiselect/views/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/password/index.d.ts +3 -3
- package/dist/declarations/src/fields/types/password/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/relationship/index.d.ts +8 -8
- package/dist/declarations/src/fields/types/relationship/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/relationship/views/ComboboxMany.d.ts +3 -3
- package/dist/declarations/src/fields/types/relationship/views/ComboboxMany.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/relationship/views/ComboboxSingle.d.ts +3 -3
- package/dist/declarations/src/fields/types/relationship/views/ComboboxSingle.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/relationship/views/index.d.ts +3 -3
- package/dist/declarations/src/fields/types/relationship/views/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/relationship/views/types.d.ts +3 -3
- package/dist/declarations/src/fields/types/relationship/views/types.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/select/index.d.ts +3 -3
- package/dist/declarations/src/fields/types/select/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/text/index.d.ts +3 -3
- package/dist/declarations/src/fields/types/text/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/timestamp/index.d.ts +3 -3
- package/dist/declarations/src/fields/types/timestamp/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/virtual/index.d.ts +7 -7
- package/dist/declarations/src/fields/types/virtual/index.d.ts.map +1 -1
- package/dist/declarations/src/helpers.d.ts +249 -13
- package/dist/declarations/src/helpers.d.ts.map +1 -1
- package/dist/declarations/src/index.d.ts +9 -4
- package/dist/declarations/src/index.d.ts.map +1 -1
- package/dist/declarations/src/internal-unstable/admin-ui/pages/ListPage/index.d.ts.map +1 -1
- package/dist/declarations/src/lib/admin-meta.d.ts +11 -11
- package/dist/declarations/src/lib/admin-meta.d.ts.map +1 -1
- package/dist/declarations/src/lib/core/access-control.d.ts +18 -18
- package/dist/declarations/src/lib/core/access-control.d.ts.map +1 -1
- package/dist/declarations/src/lib/core/cascade.d.ts +47 -0
- package/dist/declarations/src/lib/core/cascade.d.ts.map +1 -0
- package/dist/declarations/src/lib/core/initialise-lists.d.ts +27 -24
- package/dist/declarations/src/lib/core/initialise-lists.d.ts.map +1 -1
- package/dist/declarations/src/lib/env.d.ts +9 -0
- package/dist/declarations/src/lib/env.d.ts.map +1 -0
- package/dist/declarations/src/lib/system.d.ts +1 -1
- package/dist/declarations/src/lib/system.d.ts.map +1 -1
- package/dist/declarations/src/list-features.d.ts +162 -0
- package/dist/declarations/src/list-features.d.ts.map +1 -0
- package/dist/declarations/src/schema.d.ts +24 -23
- package/dist/declarations/src/schema.d.ts.map +1 -1
- package/dist/declarations/src/session.d.ts +75 -0
- package/dist/declarations/src/session.d.ts.map +1 -1
- package/dist/declarations/src/types/admin-meta.d.ts +11 -11
- package/dist/declarations/src/types/admin-meta.d.ts.map +1 -1
- package/dist/declarations/src/types/config/access-control.d.ts +42 -42
- package/dist/declarations/src/types/config/access-control.d.ts.map +1 -1
- package/dist/declarations/src/types/config/fields.d.ts +19 -19
- package/dist/declarations/src/types/config/fields.d.ts.map +1 -1
- package/dist/declarations/src/types/config/hooks.d.ts +131 -131
- package/dist/declarations/src/types/config/hooks.d.ts.map +1 -1
- package/dist/declarations/src/types/config/index.d.ts +190 -8
- package/dist/declarations/src/types/config/index.d.ts.map +1 -1
- package/dist/declarations/src/types/config/lists.d.ts +146 -108
- package/dist/declarations/src/types/config/lists.d.ts.map +1 -1
- package/dist/declarations/src/types/context.d.ts +507 -47
- package/dist/declarations/src/types/context.d.ts.map +1 -1
- package/dist/declarations/src/types/next-fields.d.ts +28 -28
- package/dist/declarations/src/types/next-fields.d.ts.map +1 -1
- package/dist/declarations/src/types/type-info.d.ts +3 -3
- package/dist/declarations/src/types/type-info.d.ts.map +1 -1
- package/dist/{express-455ae20c.cjs.js → express-84d534c2.cjs.js} +6 -6
- package/dist/{express-7559ca2d.esm.js → express-d0a4ce99.esm.js} +6 -6
- package/dist/{index-15c8f81e.esm.js → index-5d8b0b4e.esm.js} +363 -183
- package/dist/index-6055753b.cjs.js +393 -0
- package/dist/{index-42045902.cjs.js → index-ac29f382.cjs.js} +363 -185
- package/dist/index-f1703b7b.esm.js +386 -0
- package/dist/nixxie-cms-core.cjs.js +1388 -30
- package/dist/nixxie-cms-core.esm.js +1362 -24
- package/dist/{non-null-graphql-add6bb3d.cjs.js → non-null-graphql-4a44c122.cjs.js} +1 -1
- package/dist/{non-null-graphql-a84ed64d.esm.js → non-null-graphql-8c5feaae.esm.js} +1 -1
- package/dist/{resolve-hooks-165a9ce2.cjs.js → resolve-hooks-10a5f84c.cjs.js} +240 -6
- package/dist/{resolve-hooks-6813a045.esm.js → resolve-hooks-9e676794.esm.js} +238 -7
- package/dist/{system-a321642d.cjs.js → system-6b37a5f8.cjs.js} +33 -7
- package/dist/{system-03e49e4f.esm.js → system-e591d821.esm.js} +33 -7
- package/fields/dist/nixxie-cms-core-fields.cjs.js +29 -576
- package/fields/dist/nixxie-cms-core-fields.esm.js +18 -565
- package/fields/types/bytes/dist/nixxie-cms-core-fields-types-bytes.cjs.js +4 -2
- package/fields/types/bytes/dist/nixxie-cms-core-fields-types-bytes.esm.js +4 -2
- package/fields/types/multiselect/views/dist/nixxie-cms-core-fields-types-multiselect-views.cjs.js +1 -6
- package/fields/types/multiselect/views/dist/nixxie-cms-core-fields-types-multiselect-views.esm.js +1 -6
- package/fields/types/password/dist/nixxie-cms-core-fields-types-password.cjs.js +4 -2
- package/fields/types/password/dist/nixxie-cms-core-fields-types-password.esm.js +4 -2
- package/internal-unstable/artifacts/dist/nixxie-cms-core-internal-unstable-artifacts.cjs.js +4 -3
- package/internal-unstable/artifacts/dist/nixxie-cms-core-internal-unstable-artifacts.esm.js +4 -3
- package/package.json +4 -4
- package/scripts/cli/dist/nixxie-cms-core-scripts-cli.cjs.js +4 -3
- package/scripts/cli/dist/nixxie-cms-core-scripts-cli.esm.js +4 -3
- package/scripts/dist/nixxie-cms-core-scripts.cjs.js +4 -3
- package/scripts/dist/nixxie-cms-core-scripts.esm.js +4 -3
- package/session/dist/nixxie-cms-core-session.cjs.js +286 -0
- package/session/dist/nixxie-cms-core-session.esm.js +279 -1
- package/src/access.ts +25 -25
- package/src/admin-ui/admin-meta-graphql.ts +5 -5
- package/src/admin-ui/components/CreateButtonLink.tsx +46 -46
- package/src/admin-ui/components/Navigation.tsx +3 -3
- package/src/admin-ui/context.tsx +6 -6
- package/src/admin-ui/utils/Fields.tsx +241 -241
- package/src/admin-ui/utils/actionData.ts +36 -36
- package/src/admin-ui/utils/filters.ts +148 -148
- package/src/admin-ui/utils/useCreateItem.ts +171 -171
- package/src/admin-ui/utils/utils.tsx +127 -127
- package/src/context.ts +1 -1
- package/src/fields/non-null-graphql.ts +115 -115
- package/src/fields/types/bigInt/index.ts +6 -6
- package/src/fields/types/bytes/index.ts +6 -6
- package/src/fields/types/calendarDay/index.ts +18 -19
- package/src/fields/types/checkbox/index.ts +6 -6
- package/src/fields/types/decimal/index.ts +6 -6
- package/src/fields/types/file/index.ts +8 -8
- package/src/fields/types/float/index.ts +6 -6
- package/src/fields/types/image/index.ts +8 -8
- package/src/fields/types/integer/index.ts +6 -6
- package/src/fields/types/json/index.ts +5 -5
- package/src/fields/types/multiselect/index.ts +7 -7
- package/src/fields/types/multiselect/views/index.tsx +149 -151
- package/src/fields/types/password/index.ts +6 -6
- package/src/fields/types/relationship/index.ts +13 -13
- package/src/fields/types/relationship/views/ComboboxMany.tsx +110 -110
- package/src/fields/types/relationship/views/ComboboxSingle.tsx +115 -115
- package/src/fields/types/relationship/views/ContextualActions.tsx +139 -139
- package/src/fields/types/relationship/views/index.tsx +492 -492
- package/src/fields/types/relationship/views/types.ts +46 -46
- package/src/fields/types/relationship/views/useApolloQuery.ts +185 -185
- package/src/fields/types/relationship/views/useFilter.tsx +109 -109
- package/src/fields/types/select/index.ts +6 -6
- package/src/fields/types/text/index.ts +6 -6
- package/src/fields/types/timestamp/index.ts +23 -21
- package/src/fields/types/virtual/index.ts +11 -11
- package/src/helpers.ts +773 -42
- package/src/index.ts +66 -24
- package/src/internal-unstable/admin-ui/pages/ItemPage/common.tsx +4 -4
- package/src/internal-unstable/admin-ui/pages/ItemPage/index.tsx +5 -5
- package/src/internal-unstable/admin-ui/pages/ListPage/index.tsx +8 -8
- package/src/lib/admin-meta.ts +369 -369
- package/src/lib/context/createContext.ts +6 -0
- package/src/lib/core/access-control.ts +434 -434
- package/src/lib/core/cascade.ts +236 -0
- package/src/lib/core/initialise-lists.ts +49 -33
- package/src/lib/core/mutations/index.ts +7 -0
- package/src/lib/core/mutations/nested-mutation-many-input-resolvers.ts +145 -145
- package/src/lib/core/mutations/nested-mutation-one-input-resolvers.ts +71 -71
- package/src/lib/core/queries/output-field.ts +178 -178
- package/src/lib/env.ts +50 -0
- package/src/lib/id-field.ts +2 -2
- package/src/lib/system.ts +221 -207
- package/src/lib/typescript-schema-printer.ts +227 -227
- package/src/list-features.ts +476 -0
- package/src/schema.ts +92 -22
- package/src/session.ts +225 -0
- package/src/types/admin-meta.ts +218 -218
- package/src/types/config/access-control.ts +186 -186
- package/src/types/config/fields.ts +96 -96
- package/src/types/config/hooks.ts +529 -529
- package/src/types/config/index.ts +206 -7
- package/src/types/config/lists.ts +606 -565
- package/src/types/context.ts +592 -55
- package/src/types/next-fields.ts +31 -31
- package/src/types/type-info.ts +38 -38
- package/src/types/type-tests.ts +21 -21
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declarative collection features compiled into hooks + access-control filters by
|
|
3
|
+
* `defineCollection()`: computed fields, constraints, default filters, state machines,
|
|
4
|
+
* policies, events, search indexing and version snapshots.
|
|
5
|
+
*
|
|
6
|
+
* Everything here builds on the public hook/access surface — no resolver internals —
|
|
7
|
+
* so the features compose with mixins and user hooks via the same merge pipeline.
|
|
8
|
+
*/
|
|
9
|
+
import type { CollectionHooks } from './types'
|
|
10
|
+
import type { MaybePromise } from './types/utils'
|
|
11
|
+
|
|
12
|
+
type AnyArgs = any
|
|
13
|
+
|
|
14
|
+
// ── Option types ──
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Persisted derived fields, recalculated on every create/update after all other
|
|
18
|
+
* `resolveInput` hooks have run. The target keys must be real fields on the collection
|
|
19
|
+
* (unlike `virtual()` fields, computed values are stored — so they can be filtered and
|
|
20
|
+
* sorted). `merged` is the item's prospective state (existing values + this write).
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* computed: { total: ({ merged }) => merged.price * merged.quantity }
|
|
24
|
+
*/
|
|
25
|
+
export type ComputedConfig = Record<
|
|
26
|
+
string,
|
|
27
|
+
(args: {
|
|
28
|
+
operation: 'create' | 'update'
|
|
29
|
+
resolvedData: Record<string, any>
|
|
30
|
+
/** Existing item values overlaid with this operation's resolved data. */
|
|
31
|
+
merged: Record<string, any>
|
|
32
|
+
item: Record<string, any> | undefined
|
|
33
|
+
context: any
|
|
34
|
+
}) => MaybePromise<unknown>
|
|
35
|
+
>
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Data-integrity rules checked before every create/update.
|
|
39
|
+
*
|
|
40
|
+
* - `uniqueTogether`: compound uniqueness over scalar fields (e.g. `[['tenant', 'slug']]`).
|
|
41
|
+
* Checked with a query, so add a DB index for hot paths.
|
|
42
|
+
* - `checks`: named cross-field rules; return an error message to reject the write.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* constraints: {
|
|
46
|
+
* uniqueTogether: [['translationKey', 'locale']],
|
|
47
|
+
* checks: { dates: ({ merged }) => merged.endDate <= merged.startDate ? 'endDate must be after startDate' : undefined },
|
|
48
|
+
* }
|
|
49
|
+
*/
|
|
50
|
+
export type ConstraintsConfig = {
|
|
51
|
+
uniqueTogether?: string[][]
|
|
52
|
+
checks?: Record<
|
|
53
|
+
string,
|
|
54
|
+
(args: {
|
|
55
|
+
operation: 'create' | 'update'
|
|
56
|
+
resolvedData: Record<string, any>
|
|
57
|
+
merged: Record<string, any>
|
|
58
|
+
item: Record<string, any> | undefined
|
|
59
|
+
context: any
|
|
60
|
+
}) => MaybePromise<string | undefined | void>
|
|
61
|
+
>
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A where-filter automatically ANDed into every query/update/delete — including the
|
|
66
|
+
* Admin UI, which respects access filters. This is what makes `withSoftDelete()` real:
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* defaultFilter: { deletedAt: null }
|
|
70
|
+
* @example
|
|
71
|
+
* defaultFilter: ({ session }) => session?.data?.isAdmin ? true : { status: { equals: 'published' } }
|
|
72
|
+
*/
|
|
73
|
+
export type DefaultFilterConfig =
|
|
74
|
+
| Record<string, unknown>
|
|
75
|
+
| ((args: { session?: any; context: any }) => MaybePromise<boolean | Record<string, unknown>>)
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Enforce legal status transitions on a select field at the mutation layer.
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* stateMachine: {
|
|
82
|
+
* field: 'status',
|
|
83
|
+
* transitions: { draft: ['review'], review: ['published', 'draft'], published: ['archived'] },
|
|
84
|
+
* guards: { 'review->published': ({ session }) => session?.data?.role === 'editor' || 'Only editors can publish' },
|
|
85
|
+
* }
|
|
86
|
+
*/
|
|
87
|
+
export type StateMachineConfig = {
|
|
88
|
+
/** The (select) field holding the state. */
|
|
89
|
+
field: string
|
|
90
|
+
/** Allowed transitions: from-state → list of reachable states. */
|
|
91
|
+
transitions: Record<string, string[]>
|
|
92
|
+
/**
|
|
93
|
+
* Optional guards keyed `"from->to"`. Return `false` to block (generic message)
|
|
94
|
+
* or a string to block with that message.
|
|
95
|
+
*/
|
|
96
|
+
guards?: Record<
|
|
97
|
+
string,
|
|
98
|
+
(args: {
|
|
99
|
+
item: Record<string, any>
|
|
100
|
+
resolvedData: Record<string, any>
|
|
101
|
+
context: any
|
|
102
|
+
session?: any
|
|
103
|
+
}) => MaybePromise<boolean | string>
|
|
104
|
+
>
|
|
105
|
+
/** States allowed on create. Omit to allow any state on create. */
|
|
106
|
+
initial?: string | string[]
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** One row-level access rule: the first rule whose `when` matches decides the filter. */
|
|
110
|
+
export type PolicyRule = {
|
|
111
|
+
/** Does this rule apply to the session? (e.g. `session => session?.data?.role === 'editor'`) */
|
|
112
|
+
when: (session: any) => boolean
|
|
113
|
+
/** `true` = unrestricted, `false` = no access, object = where-filter, function = computed filter. */
|
|
114
|
+
filter:
|
|
115
|
+
| boolean
|
|
116
|
+
| Record<string, unknown>
|
|
117
|
+
| ((session: any) => boolean | Record<string, unknown>)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Declarative row-level access: per operation, rules are evaluated in order and the
|
|
122
|
+
* first matching `when` supplies the filter; no match means no access. ANDed with any
|
|
123
|
+
* explicit `access.filter` you also configure. Pairs naturally with @nixxie-cms/rbac.
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* policies: {
|
|
127
|
+
* query: [
|
|
128
|
+
* { when: s => s?.data?.role === 'admin', filter: true },
|
|
129
|
+
* { when: s => !!s, filter: s => ({ author: { id: { equals: s.itemId } } }) },
|
|
130
|
+
* ],
|
|
131
|
+
* }
|
|
132
|
+
*/
|
|
133
|
+
export type PoliciesConfig = Partial<Record<'query' | 'update' | 'delete', PolicyRule[]>>
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Emit lifecycle events through `context.services.webhooks` after every write:
|
|
137
|
+
* `<prefix>.created` / `.updated` / `.deleted` (prefix defaults to the list key).
|
|
138
|
+
* No-op when the webhooks service is not configured.
|
|
139
|
+
*/
|
|
140
|
+
export type EventsConfig =
|
|
141
|
+
| true
|
|
142
|
+
| {
|
|
143
|
+
/** Event name prefix. @default the list key */
|
|
144
|
+
prefix?: string
|
|
145
|
+
operations?: ('create' | 'update' | 'delete')[]
|
|
146
|
+
/** Customise the event payload. @default { listKey, id, item } */
|
|
147
|
+
payload?: (args: AnyArgs) => unknown
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Keep a search index in sync through `context.services.search`: documents are indexed
|
|
152
|
+
* after create/update and removed after delete. No-op when search is not configured.
|
|
153
|
+
*/
|
|
154
|
+
export type SearchableConfig =
|
|
155
|
+
| true
|
|
156
|
+
| {
|
|
157
|
+
/** Index name. @default the list key lowercased */
|
|
158
|
+
index?: string
|
|
159
|
+
/** Item fields copied into the document. @default all scalar values on the item */
|
|
160
|
+
fields?: string[]
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Snapshot every create/update into `context.services.versioning` (resource = list key).
|
|
165
|
+
* No-op when versioning is not configured.
|
|
166
|
+
*/
|
|
167
|
+
export type VersionedConfig =
|
|
168
|
+
| true
|
|
169
|
+
| {
|
|
170
|
+
/** Optional label for each snapshot. */
|
|
171
|
+
label?: (args: AnyArgs) => string | undefined
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export type ListFeaturesConfig = {
|
|
175
|
+
computed?: ComputedConfig
|
|
176
|
+
constraints?: ConstraintsConfig
|
|
177
|
+
defaultFilter?: DefaultFilterConfig
|
|
178
|
+
stateMachine?: StateMachineConfig
|
|
179
|
+
policies?: PoliciesConfig
|
|
180
|
+
events?: EventsConfig
|
|
181
|
+
searchable?: SearchableConfig
|
|
182
|
+
versioned?: VersionedConfig
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ── Compilation to hooks ──
|
|
186
|
+
|
|
187
|
+
const mergedView = (args: AnyArgs): Record<string, any> => ({
|
|
188
|
+
...(args.item ?? {}),
|
|
189
|
+
...(args.resolvedData ?? {}),
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
function computedHooks(computed: ComputedConfig): Partial<CollectionHooks<any>> {
|
|
193
|
+
return {
|
|
194
|
+
resolveInput: async (args: AnyArgs) => {
|
|
195
|
+
const out = { ...args.resolvedData }
|
|
196
|
+
for (const [field, fn] of Object.entries(computed)) {
|
|
197
|
+
out[field] = await fn({
|
|
198
|
+
operation: args.operation,
|
|
199
|
+
resolvedData: out,
|
|
200
|
+
merged: { ...(args.item ?? {}), ...out },
|
|
201
|
+
item: args.item,
|
|
202
|
+
context: args.context,
|
|
203
|
+
})
|
|
204
|
+
}
|
|
205
|
+
return out
|
|
206
|
+
},
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function constraintsHooks(constraints: ConstraintsConfig): Partial<CollectionHooks<any>> {
|
|
211
|
+
return {
|
|
212
|
+
validate: async (args: AnyArgs) => {
|
|
213
|
+
const { operation, addValidationError } = args
|
|
214
|
+
if (operation === 'delete') return
|
|
215
|
+
const merged = mergedView(args)
|
|
216
|
+
|
|
217
|
+
for (const tuple of constraints.uniqueTogether ?? []) {
|
|
218
|
+
const values = tuple.map(field => merged[field])
|
|
219
|
+
// Incomplete tuples (a NULL member) are not checked — same semantics as SQL unique indexes.
|
|
220
|
+
if (values.some(value => value === undefined || value === null)) continue
|
|
221
|
+
const where: Record<string, any> = {}
|
|
222
|
+
tuple.forEach((field, i) => (where[field] = { equals: values[i] }))
|
|
223
|
+
try {
|
|
224
|
+
const clashes = await args.context.sudo().db[args.listKey].findMany({ where, take: 2 })
|
|
225
|
+
const other = clashes.find(
|
|
226
|
+
(clash: any) => !args.item || String(clash.id) !== String(args.item.id)
|
|
227
|
+
)
|
|
228
|
+
if (other) {
|
|
229
|
+
addValidationError(`Another item already exists with the same ${tuple.join(' + ')}`)
|
|
230
|
+
}
|
|
231
|
+
} catch (err) {
|
|
232
|
+
// A malformed tuple (unknown/non-scalar field) should fail loudly, not silently pass.
|
|
233
|
+
addValidationError(
|
|
234
|
+
`uniqueTogether check for (${tuple.join(', ')}) failed: ${err instanceof Error ? err.message : err}`
|
|
235
|
+
)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
for (const check of Object.values(constraints.checks ?? {})) {
|
|
240
|
+
const message = await check({
|
|
241
|
+
operation: args.operation,
|
|
242
|
+
resolvedData: args.resolvedData,
|
|
243
|
+
merged,
|
|
244
|
+
item: args.item,
|
|
245
|
+
context: args.context,
|
|
246
|
+
})
|
|
247
|
+
if (typeof message === 'string' && message.length > 0) addValidationError(message)
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function stateMachineHooks(machine: StateMachineConfig): Partial<CollectionHooks<any>> {
|
|
254
|
+
const { field, transitions, guards = {} } = machine
|
|
255
|
+
const initial =
|
|
256
|
+
machine.initial === undefined
|
|
257
|
+
? undefined
|
|
258
|
+
: Array.isArray(machine.initial)
|
|
259
|
+
? machine.initial
|
|
260
|
+
: [machine.initial]
|
|
261
|
+
return {
|
|
262
|
+
validate: async (args: AnyArgs) => {
|
|
263
|
+
const { operation, resolvedData, item, addValidationError } = args
|
|
264
|
+
if (operation === 'delete') return
|
|
265
|
+
const next = resolvedData?.[field]
|
|
266
|
+
if (next === undefined) return
|
|
267
|
+
|
|
268
|
+
if (operation === 'create') {
|
|
269
|
+
if (initial && next != null && !initial.includes(next)) {
|
|
270
|
+
addValidationError(
|
|
271
|
+
`"${field}" cannot start as "${next}" (allowed: ${initial.join(', ')})`
|
|
272
|
+
)
|
|
273
|
+
}
|
|
274
|
+
return
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const previous = item?.[field]
|
|
278
|
+
if (next === previous) return
|
|
279
|
+
const allowed = transitions[previous] ?? []
|
|
280
|
+
if (!allowed.includes(next)) {
|
|
281
|
+
addValidationError(
|
|
282
|
+
`"${field}" cannot change from "${previous}" to "${next}"` +
|
|
283
|
+
(allowed.length ? ` (allowed: ${allowed.join(', ')})` : ' (no transitions from this state)')
|
|
284
|
+
)
|
|
285
|
+
return
|
|
286
|
+
}
|
|
287
|
+
const guard = guards[`${previous}->${next}`]
|
|
288
|
+
if (guard) {
|
|
289
|
+
const verdict = await guard({
|
|
290
|
+
item,
|
|
291
|
+
resolvedData,
|
|
292
|
+
context: args.context,
|
|
293
|
+
session: args.context?.session,
|
|
294
|
+
})
|
|
295
|
+
if (verdict !== true) {
|
|
296
|
+
addValidationError(
|
|
297
|
+
typeof verdict === 'string'
|
|
298
|
+
? verdict
|
|
299
|
+
: `"${field}" transition "${previous}" → "${next}" was blocked`
|
|
300
|
+
)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const pastTense = { create: 'created', update: 'updated', delete: 'deleted' } as const
|
|
308
|
+
|
|
309
|
+
function eventsHooks(events: EventsConfig): Partial<CollectionHooks<any>> {
|
|
310
|
+
const options = events === true ? {} : events
|
|
311
|
+
const operations = options.operations ?? ['create', 'update', 'delete']
|
|
312
|
+
return {
|
|
313
|
+
afterOperation: async (args: AnyArgs) => {
|
|
314
|
+
if (!operations.includes(args.operation)) return
|
|
315
|
+
const webhooks = args.context?.services?.webhooks
|
|
316
|
+
if (!webhooks) return
|
|
317
|
+
try {
|
|
318
|
+
const subject = args.item ?? args.originalItem
|
|
319
|
+
const event = `${options.prefix ?? args.listKey}.${pastTense[args.operation as keyof typeof pastTense]}`
|
|
320
|
+
const payload = options.payload
|
|
321
|
+
? await options.payload(args)
|
|
322
|
+
: { listKey: args.listKey, id: subject?.id != null ? String(subject.id) : undefined, item: subject }
|
|
323
|
+
await webhooks.trigger(event, payload)
|
|
324
|
+
} catch (err) {
|
|
325
|
+
console.error(`[nixxie] events: failed to emit for ${args.listKey}:`, err)
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Copy the indexable scalar values off an item (used when `fields` is not specified). */
|
|
332
|
+
function scalarDocument(item: Record<string, any>): Record<string, unknown> {
|
|
333
|
+
const doc: Record<string, unknown> = {}
|
|
334
|
+
for (const [key, value] of Object.entries(item)) {
|
|
335
|
+
if (key.startsWith('__')) continue
|
|
336
|
+
if (value === null) doc[key] = null
|
|
337
|
+
else if (value instanceof Date) doc[key] = value.toISOString()
|
|
338
|
+
else if (['string', 'number', 'boolean'].includes(typeof value)) doc[key] = value
|
|
339
|
+
}
|
|
340
|
+
return doc
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function searchableHooks(searchable: SearchableConfig): Partial<CollectionHooks<any>> {
|
|
344
|
+
const options = searchable === true ? {} : searchable
|
|
345
|
+
return {
|
|
346
|
+
afterOperation: async (args: AnyArgs) => {
|
|
347
|
+
const search = args.context?.services?.search
|
|
348
|
+
if (!search) return
|
|
349
|
+
const indexName = options.index ?? String(args.listKey).toLowerCase()
|
|
350
|
+
try {
|
|
351
|
+
if (args.operation === 'delete') {
|
|
352
|
+
await search.remove(indexName, String(args.originalItem.id))
|
|
353
|
+
return
|
|
354
|
+
}
|
|
355
|
+
const item = args.item
|
|
356
|
+
const picked = options.fields
|
|
357
|
+
? Object.fromEntries(
|
|
358
|
+
options.fields.map(field => [
|
|
359
|
+
field,
|
|
360
|
+
item[field] instanceof Date ? item[field].toISOString() : item[field],
|
|
361
|
+
])
|
|
362
|
+
)
|
|
363
|
+
: scalarDocument(item)
|
|
364
|
+
await search.index(indexName, { ...picked, id: String(item.id) })
|
|
365
|
+
} catch (err) {
|
|
366
|
+
console.error(`[nixxie] searchable: failed to sync index "${indexName}":`, err)
|
|
367
|
+
}
|
|
368
|
+
},
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function versionedHooks(versioned: VersionedConfig): Partial<CollectionHooks<any>> {
|
|
373
|
+
const options = versioned === true ? {} : versioned
|
|
374
|
+
return {
|
|
375
|
+
afterOperation: async (args: AnyArgs) => {
|
|
376
|
+
if (args.operation === 'delete') return
|
|
377
|
+
const versioning = args.context?.services?.versioning
|
|
378
|
+
if (!versioning) return
|
|
379
|
+
try {
|
|
380
|
+
const session = args.context?.session
|
|
381
|
+
await versioning.snapshot({
|
|
382
|
+
resource: args.listKey,
|
|
383
|
+
resourceId: String(args.item.id),
|
|
384
|
+
data: scalarSafe(args.item),
|
|
385
|
+
label: options.label?.(args),
|
|
386
|
+
actor: session?.itemId != null ? { id: String(session.itemId) } : undefined,
|
|
387
|
+
})
|
|
388
|
+
} catch (err) {
|
|
389
|
+
console.error(`[nixxie] versioned: failed to snapshot ${args.listKey}:`, err)
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** Snapshot-safe clone: keeps JSON-representable values, ISO-stringifies dates. */
|
|
396
|
+
function scalarSafe(item: Record<string, any>): Record<string, unknown> {
|
|
397
|
+
const out: Record<string, unknown> = {}
|
|
398
|
+
for (const [key, value] of Object.entries(item)) {
|
|
399
|
+
if (value instanceof Date) out[key] = value.toISOString()
|
|
400
|
+
else if (typeof value === 'bigint') out[key] = value.toString()
|
|
401
|
+
else if (typeof value !== 'function' && typeof value !== 'symbol') out[key] = value
|
|
402
|
+
}
|
|
403
|
+
return out
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** Compile the declarative features into hook fragments, in a deliberate order. */
|
|
407
|
+
export function compileFeatureHooks(features: ListFeaturesConfig): Array<Partial<CollectionHooks<any>>> {
|
|
408
|
+
const hooks: Array<Partial<CollectionHooks<any>>> = []
|
|
409
|
+
if (features.computed) hooks.push(computedHooks(features.computed))
|
|
410
|
+
if (features.constraints) hooks.push(constraintsHooks(features.constraints))
|
|
411
|
+
if (features.stateMachine) hooks.push(stateMachineHooks(features.stateMachine))
|
|
412
|
+
if (features.events) hooks.push(eventsHooks(features.events))
|
|
413
|
+
if (features.searchable) hooks.push(searchableHooks(features.searchable))
|
|
414
|
+
if (features.versioned) hooks.push(versionedHooks(features.versioned))
|
|
415
|
+
return hooks
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// ── Compilation to access filters ──
|
|
419
|
+
|
|
420
|
+
type FilterValue = boolean | Record<string, unknown>
|
|
421
|
+
type FilterLike = FilterValue | ((args: any) => MaybePromise<FilterValue>)
|
|
422
|
+
|
|
423
|
+
/** AND two access filters; `true`/missing means unrestricted, `false` wins outright. */
|
|
424
|
+
function andFilters(a: FilterLike | undefined, b: FilterLike): FilterLike {
|
|
425
|
+
if (a === undefined) return b
|
|
426
|
+
return async (args: any) => {
|
|
427
|
+
const ra = typeof a === 'function' ? await a(args) : a
|
|
428
|
+
const rb = typeof b === 'function' ? await b(args) : b
|
|
429
|
+
if (ra === false || rb === false) return false
|
|
430
|
+
if (ra === true) return rb
|
|
431
|
+
if (rb === true) return ra
|
|
432
|
+
return { AND: [ra, rb] }
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function policyFilter(rules: PolicyRule[]): FilterLike {
|
|
437
|
+
return ({ session }: any) => {
|
|
438
|
+
for (const rule of rules) {
|
|
439
|
+
if (!rule.when(session)) continue
|
|
440
|
+
return typeof rule.filter === 'function' ? rule.filter(session) : rule.filter
|
|
441
|
+
}
|
|
442
|
+
return false
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Merge `defaultFilter` and `policies` into an access-control object. Existing filters
|
|
448
|
+
* are preserved and ANDed with the feature filters.
|
|
449
|
+
*/
|
|
450
|
+
export function applyAccessFeatures(
|
|
451
|
+
access: any,
|
|
452
|
+
features: Pick<ListFeaturesConfig, 'defaultFilter' | 'policies'>
|
|
453
|
+
): any {
|
|
454
|
+
const { defaultFilter, policies } = features
|
|
455
|
+
if (!defaultFilter && !policies) return access
|
|
456
|
+
|
|
457
|
+
const filter = { ...(access?.filter ?? {}) }
|
|
458
|
+
for (const operation of ['query', 'update', 'delete'] as const) {
|
|
459
|
+
let merged: FilterLike | undefined = filter[operation]
|
|
460
|
+
if (defaultFilter) {
|
|
461
|
+
merged = andFilters(
|
|
462
|
+
merged,
|
|
463
|
+
typeof defaultFilter === 'function'
|
|
464
|
+
? (args: any) => defaultFilter({ session: args.session, context: args.context })
|
|
465
|
+
: (defaultFilter as Record<string, unknown>)
|
|
466
|
+
)
|
|
467
|
+
}
|
|
468
|
+
const rules = policies?.[operation]
|
|
469
|
+
if (rules?.length) {
|
|
470
|
+
merged = andFilters(merged, policyFilter(rules))
|
|
471
|
+
}
|
|
472
|
+
if (merged !== undefined) filter[operation] = merged
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return { ...(access ?? {}), filter }
|
|
476
|
+
}
|
package/src/schema.ts
CHANGED
|
@@ -1,26 +1,80 @@
|
|
|
1
1
|
import type { ListenOptions } from 'node:net'
|
|
2
2
|
|
|
3
|
+
import { validateEnv } from './lib/env'
|
|
3
4
|
import { idFieldType } from './lib/id-field'
|
|
4
5
|
import type {
|
|
5
6
|
Action,
|
|
6
7
|
ActionArgsConfig,
|
|
7
8
|
BaseFields,
|
|
8
9
|
BaseNixxieTypeInfo,
|
|
9
|
-
|
|
10
|
+
BaseCollectionTypeInfo,
|
|
10
11
|
DeclaredAction,
|
|
11
12
|
IdFieldConfig,
|
|
12
13
|
NixxieConfig,
|
|
13
14
|
NixxieConfigPre,
|
|
14
15
|
NixxieContext,
|
|
15
|
-
|
|
16
|
+
CollectionConfig,
|
|
16
17
|
MaybeItemFunctionWithFilter,
|
|
17
18
|
MaybeSessionFunction,
|
|
18
19
|
MaybeSessionFunctionWithFilter,
|
|
19
20
|
} from './types'
|
|
20
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Fold `config.plugins` into the config: merge plugin collections (key conflicts throw),
|
|
24
|
+
* run each plugin's `extendConfig`, and chain plugin `onConnect`s before `db.onConnect`.
|
|
25
|
+
*/
|
|
26
|
+
function applyPlugins<TypeInfo extends BaseNixxieTypeInfo>(
|
|
27
|
+
config: NixxieConfigPre<TypeInfo>
|
|
28
|
+
): NixxieConfigPre<TypeInfo> {
|
|
29
|
+
const plugins = config.plugins ?? []
|
|
30
|
+
if (!plugins.length) return config
|
|
31
|
+
|
|
32
|
+
const seen = new Set<string>()
|
|
33
|
+
for (const plugin of plugins) {
|
|
34
|
+
if (!plugin.name) throw new Error('Every plugin must have a `name`')
|
|
35
|
+
if (seen.has(plugin.name)) throw new Error(`Duplicate plugin name "${plugin.name}"`)
|
|
36
|
+
seen.add(plugin.name)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let next: NixxieConfigPre<TypeInfo> = { ...config, collections: { ...config.collections } }
|
|
40
|
+
for (const plugin of plugins) {
|
|
41
|
+
for (const [key, collection] of Object.entries(plugin.collections ?? {})) {
|
|
42
|
+
if (key in next.collections) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`Plugin "${plugin.name}" adds the collection "${key}", but a collection with that key already exists`
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
next.collections[key] = collection
|
|
48
|
+
}
|
|
49
|
+
if (plugin.extendConfig) {
|
|
50
|
+
next = plugin.extendConfig(next)
|
|
51
|
+
if (!next) {
|
|
52
|
+
throw new Error(`Plugin "${plugin.name}".extendConfig must return the config`)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const onConnects = plugins.flatMap(plugin => (plugin.onConnect ? [plugin.onConnect] : []))
|
|
58
|
+
if (onConnects.length) {
|
|
59
|
+
const userOnConnect = next.db.onConnect
|
|
60
|
+
next = {
|
|
61
|
+
...next,
|
|
62
|
+
db: {
|
|
63
|
+
...next.db,
|
|
64
|
+
onConnect: async context => {
|
|
65
|
+
for (const fn of onConnects) await fn(context)
|
|
66
|
+
await userOnConnect?.(context)
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return next
|
|
73
|
+
}
|
|
74
|
+
|
|
21
75
|
function listsWithDefaults(config: NixxieConfigPre, defaultIdField: IdFieldConfig) {
|
|
22
76
|
// some error checking
|
|
23
|
-
for (const [listKey, list] of Object.entries(config.
|
|
77
|
+
for (const [listKey, list] of Object.entries(config.collections)) {
|
|
24
78
|
if (list.fields.id) {
|
|
25
79
|
throw new Error(
|
|
26
80
|
`"fields.id" is reserved by Nixxie, use "db.idField" for the "${listKey}" list`
|
|
@@ -34,7 +88,7 @@ function listsWithDefaults(config: NixxieConfigPre, defaultIdField: IdFieldConfi
|
|
|
34
88
|
|
|
35
89
|
return Object.fromEntries([
|
|
36
90
|
...(function* () {
|
|
37
|
-
for (const [listKey, list] of Object.entries(config.
|
|
91
|
+
for (const [listKey, list] of Object.entries(config.collections)) {
|
|
38
92
|
yield [
|
|
39
93
|
listKey,
|
|
40
94
|
{
|
|
@@ -42,6 +96,7 @@ function listsWithDefaults(config: NixxieConfigPre, defaultIdField: IdFieldConfi
|
|
|
42
96
|
defaultIsFilterable: true, // TODO: move to access control?
|
|
43
97
|
defaultIsOrderable: true, // TODO: move to access control?
|
|
44
98
|
isSingleton: false,
|
|
99
|
+
cascade: [],
|
|
45
100
|
...list,
|
|
46
101
|
db: {
|
|
47
102
|
...list.db,
|
|
@@ -86,9 +141,12 @@ function identity<T>(x: T) {
|
|
|
86
141
|
return x
|
|
87
142
|
}
|
|
88
143
|
|
|
89
|
-
export function
|
|
144
|
+
export function buildConfig<TypeInfo extends BaseNixxieTypeInfo>(
|
|
90
145
|
config: NixxieConfigPre<TypeInfo>
|
|
91
146
|
): NixxieConfig<TypeInfo> {
|
|
147
|
+
if (config.env) validateEnv(config.env)
|
|
148
|
+
config = applyPlugins(config)
|
|
149
|
+
|
|
92
150
|
if (!['postgresql', 'sqlite', 'mysql'].includes(config.db.provider)) {
|
|
93
151
|
throw new TypeError(`"db.provider" only supports "sqlite", "postgresql" or "mysql"`)
|
|
94
152
|
}
|
|
@@ -162,6 +220,12 @@ export function config<TypeInfo extends BaseNixxieTypeInfo>(
|
|
|
162
220
|
search: config.search,
|
|
163
221
|
notifications: config.notifications,
|
|
164
222
|
ai: config.ai,
|
|
223
|
+
aiRag: config.aiRag,
|
|
224
|
+
versioning: config.versioning,
|
|
225
|
+
workflow: config.workflow,
|
|
226
|
+
apiKeys: config.apiKeys,
|
|
227
|
+
logger: config.logger,
|
|
228
|
+
backup: config.backup,
|
|
165
229
|
telemetry: config.telemetry ?? true,
|
|
166
230
|
ui: {
|
|
167
231
|
...config.ui,
|
|
@@ -177,34 +241,38 @@ export function config<TypeInfo extends BaseNixxieTypeInfo>(
|
|
|
177
241
|
|
|
178
242
|
let i = 0
|
|
179
243
|
|
|
180
|
-
export type GroupInfo<
|
|
244
|
+
export type GroupInfo<CollectionTypeInfo extends BaseCollectionTypeInfo> = {
|
|
181
245
|
fields: string[]
|
|
182
246
|
label: string
|
|
183
247
|
description: string
|
|
184
|
-
ui: GroupUIConfig<
|
|
248
|
+
ui: GroupUIConfig<CollectionTypeInfo> | undefined
|
|
185
249
|
}
|
|
186
250
|
|
|
187
|
-
type GroupUIConfig<
|
|
251
|
+
type GroupUIConfig<CollectionTypeInfo extends BaseCollectionTypeInfo> = {
|
|
188
252
|
createView?: {
|
|
189
|
-
defaultFieldMode?: MaybeSessionFunctionWithFilter<
|
|
253
|
+
defaultFieldMode?: MaybeSessionFunctionWithFilter<
|
|
254
|
+
'hidden' | 'edit',
|
|
255
|
+
'hidden',
|
|
256
|
+
CollectionTypeInfo
|
|
257
|
+
>
|
|
190
258
|
}
|
|
191
259
|
itemView?: {
|
|
192
260
|
defaultFieldMode?: MaybeItemFunctionWithFilter<
|
|
193
261
|
'edit' | 'hidden' | 'read',
|
|
194
262
|
'hidden' | 'read',
|
|
195
|
-
|
|
263
|
+
CollectionTypeInfo
|
|
196
264
|
>
|
|
197
265
|
}
|
|
198
266
|
listView?: {
|
|
199
|
-
defaultFieldMode?: MaybeSessionFunction<'read' | 'hidden',
|
|
267
|
+
defaultFieldMode?: MaybeSessionFunction<'read' | 'hidden', CollectionTypeInfo>
|
|
200
268
|
}
|
|
201
269
|
}
|
|
202
270
|
|
|
203
|
-
export function
|
|
271
|
+
export function fieldGroup<CollectionTypeInfo extends BaseCollectionTypeInfo>(config: {
|
|
204
272
|
label: string
|
|
205
273
|
description?: string
|
|
206
|
-
ui?: GroupUIConfig<
|
|
207
|
-
fields: BaseFields<
|
|
274
|
+
ui?: GroupUIConfig<CollectionTypeInfo>
|
|
275
|
+
fields: BaseFields<CollectionTypeInfo>
|
|
208
276
|
}) {
|
|
209
277
|
const keys = Object.keys(config.fields)
|
|
210
278
|
if (keys.some(key => key.startsWith('__group'))) {
|
|
@@ -217,21 +285,23 @@ export function group<ListTypeInfo extends BaseListTypeInfo>(config: {
|
|
|
217
285
|
label: config.label,
|
|
218
286
|
description: config.description ?? '',
|
|
219
287
|
ui: config.ui,
|
|
220
|
-
} satisfies GroupInfo<
|
|
288
|
+
} satisfies GroupInfo<CollectionTypeInfo>,
|
|
221
289
|
...config.fields,
|
|
222
|
-
} as BaseFields<
|
|
290
|
+
} as BaseFields<CollectionTypeInfo> // TODO: FIXME, see initialise-lists.ts:getListsWithInitialisedFields
|
|
223
291
|
}
|
|
224
292
|
|
|
225
|
-
export function
|
|
293
|
+
export function collection<CollectionTypeInfo extends BaseCollectionTypeInfo>(
|
|
294
|
+
listConfig: CollectionConfig<CollectionTypeInfo>
|
|
295
|
+
) {
|
|
226
296
|
return { ...listConfig }
|
|
227
297
|
}
|
|
228
298
|
|
|
229
|
-
export function
|
|
230
|
-
|
|
231
|
-
Args extends ActionArgsConfig<
|
|
232
|
-
>(action: Action<
|
|
299
|
+
export function createAction<
|
|
300
|
+
CollectionTypeInfo extends BaseCollectionTypeInfo,
|
|
301
|
+
Args extends ActionArgsConfig<CollectionTypeInfo> | undefined = undefined,
|
|
302
|
+
>(action: Action<CollectionTypeInfo, Args>): DeclaredAction<CollectionTypeInfo> {
|
|
233
303
|
return {
|
|
234
304
|
...action,
|
|
235
305
|
___defineActionsWithActionFunction: true,
|
|
236
|
-
} as unknown as DeclaredAction<
|
|
306
|
+
} as unknown as DeclaredAction<CollectionTypeInfo>
|
|
237
307
|
}
|