@nixxie-cms/core 1.0.3 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +171 -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 +349 -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-7559ca2d.esm.js → express-0abbce07.esm.js} +6 -6
- package/dist/{express-455ae20c.cjs.js → express-7ca6f76a.cjs.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 +1387 -30
- package/dist/nixxie-cms-core.esm.js +1361 -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-03e49e4f.esm.js → system-4d2a2648.esm.js} +32 -7
- package/dist/{system-a321642d.cjs.js → system-69e1a285.cjs.js} +32 -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 +5 -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 +91 -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 +185 -7
- package/src/types/config/lists.ts +606 -565
- package/src/types/context.ts +426 -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,11 @@ export function config<TypeInfo extends BaseNixxieTypeInfo>(
|
|
|
162
220
|
search: config.search,
|
|
163
221
|
notifications: config.notifications,
|
|
164
222
|
ai: config.ai,
|
|
223
|
+
versioning: config.versioning,
|
|
224
|
+
workflow: config.workflow,
|
|
225
|
+
apiKeys: config.apiKeys,
|
|
226
|
+
logger: config.logger,
|
|
227
|
+
backup: config.backup,
|
|
165
228
|
telemetry: config.telemetry ?? true,
|
|
166
229
|
ui: {
|
|
167
230
|
...config.ui,
|
|
@@ -177,34 +240,38 @@ export function config<TypeInfo extends BaseNixxieTypeInfo>(
|
|
|
177
240
|
|
|
178
241
|
let i = 0
|
|
179
242
|
|
|
180
|
-
export type GroupInfo<
|
|
243
|
+
export type GroupInfo<CollectionTypeInfo extends BaseCollectionTypeInfo> = {
|
|
181
244
|
fields: string[]
|
|
182
245
|
label: string
|
|
183
246
|
description: string
|
|
184
|
-
ui: GroupUIConfig<
|
|
247
|
+
ui: GroupUIConfig<CollectionTypeInfo> | undefined
|
|
185
248
|
}
|
|
186
249
|
|
|
187
|
-
type GroupUIConfig<
|
|
250
|
+
type GroupUIConfig<CollectionTypeInfo extends BaseCollectionTypeInfo> = {
|
|
188
251
|
createView?: {
|
|
189
|
-
defaultFieldMode?: MaybeSessionFunctionWithFilter<
|
|
252
|
+
defaultFieldMode?: MaybeSessionFunctionWithFilter<
|
|
253
|
+
'hidden' | 'edit',
|
|
254
|
+
'hidden',
|
|
255
|
+
CollectionTypeInfo
|
|
256
|
+
>
|
|
190
257
|
}
|
|
191
258
|
itemView?: {
|
|
192
259
|
defaultFieldMode?: MaybeItemFunctionWithFilter<
|
|
193
260
|
'edit' | 'hidden' | 'read',
|
|
194
261
|
'hidden' | 'read',
|
|
195
|
-
|
|
262
|
+
CollectionTypeInfo
|
|
196
263
|
>
|
|
197
264
|
}
|
|
198
265
|
listView?: {
|
|
199
|
-
defaultFieldMode?: MaybeSessionFunction<'read' | 'hidden',
|
|
266
|
+
defaultFieldMode?: MaybeSessionFunction<'read' | 'hidden', CollectionTypeInfo>
|
|
200
267
|
}
|
|
201
268
|
}
|
|
202
269
|
|
|
203
|
-
export function
|
|
270
|
+
export function fieldGroup<CollectionTypeInfo extends BaseCollectionTypeInfo>(config: {
|
|
204
271
|
label: string
|
|
205
272
|
description?: string
|
|
206
|
-
ui?: GroupUIConfig<
|
|
207
|
-
fields: BaseFields<
|
|
273
|
+
ui?: GroupUIConfig<CollectionTypeInfo>
|
|
274
|
+
fields: BaseFields<CollectionTypeInfo>
|
|
208
275
|
}) {
|
|
209
276
|
const keys = Object.keys(config.fields)
|
|
210
277
|
if (keys.some(key => key.startsWith('__group'))) {
|
|
@@ -217,21 +284,23 @@ export function group<ListTypeInfo extends BaseListTypeInfo>(config: {
|
|
|
217
284
|
label: config.label,
|
|
218
285
|
description: config.description ?? '',
|
|
219
286
|
ui: config.ui,
|
|
220
|
-
} satisfies GroupInfo<
|
|
287
|
+
} satisfies GroupInfo<CollectionTypeInfo>,
|
|
221
288
|
...config.fields,
|
|
222
|
-
} as BaseFields<
|
|
289
|
+
} as BaseFields<CollectionTypeInfo> // TODO: FIXME, see initialise-lists.ts:getListsWithInitialisedFields
|
|
223
290
|
}
|
|
224
291
|
|
|
225
|
-
export function
|
|
292
|
+
export function collection<CollectionTypeInfo extends BaseCollectionTypeInfo>(
|
|
293
|
+
listConfig: CollectionConfig<CollectionTypeInfo>
|
|
294
|
+
) {
|
|
226
295
|
return { ...listConfig }
|
|
227
296
|
}
|
|
228
297
|
|
|
229
|
-
export function
|
|
230
|
-
|
|
231
|
-
Args extends ActionArgsConfig<
|
|
232
|
-
>(action: Action<
|
|
298
|
+
export function createAction<
|
|
299
|
+
CollectionTypeInfo extends BaseCollectionTypeInfo,
|
|
300
|
+
Args extends ActionArgsConfig<CollectionTypeInfo> | undefined = undefined,
|
|
301
|
+
>(action: Action<CollectionTypeInfo, Args>): DeclaredAction<CollectionTypeInfo> {
|
|
233
302
|
return {
|
|
234
303
|
...action,
|
|
235
304
|
___defineActionsWithActionFunction: true,
|
|
236
|
-
} as unknown as DeclaredAction<
|
|
305
|
+
} as unknown as DeclaredAction<CollectionTypeInfo>
|
|
237
306
|
}
|