@opensaas/stack-core 0.27.1 → 0.29.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.
@@ -4,5 +4,5 @@ export type { Fragment, FieldSelection } from './query/index.js';
4
4
  export { hashPassword, comparePassword, isHashedPassword, HashedPassword, } from './utils/password.js';
5
5
  export { pascalToCamel, pascalToKebab, kebabToPascal, kebabToCamel } from './lib/case-utils.js';
6
6
  export { validateWithZod, generateZodSchema } from './validation/schema.js';
7
- export type { DatabaseConfig, SessionConfig, ThemeConfig, ThemePreset, ThemeColors, FileMetadata, ImageMetadata, ImageTransformationResult, } from './config/index.js';
7
+ export type { DatabaseConfig, SessionConfig, ThemeConfig, ThemePreset, ThemeColors, ThemeFonts, ThemeShadows, FileMetadata, ImageMetadata, ImageTransformationResult, } from './config/index.js';
8
8
  //# sourceMappingURL=internal.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"internal.d.ts","sourceRoot":"","sources":["../src/internal.ts"],"names":[],"mappings":"AAWA,YAAY,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAC3D,YAAY,EACV,gBAAgB,EAChB,kBAAkB,EAClB,YAAY,EACZ,iBAAiB,EACjB,mBAAmB,EACnB,iBAAiB,GAClB,MAAM,mBAAmB,CAAA;AAG1B,YAAY,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAGhE,OAAO,EACL,YAAY,EACZ,eAAe,EACf,gBAAgB,EAChB,cAAc,GACf,MAAM,qBAAqB,CAAA;AAG5B,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAG/F,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAG3E,YAAY,EACV,cAAc,EACd,aAAa,EACb,WAAW,EACX,WAAW,EACX,WAAW,EACX,YAAY,EACZ,aAAa,EACb,yBAAyB,GAC1B,MAAM,mBAAmB,CAAA"}
1
+ {"version":3,"file":"internal.d.ts","sourceRoot":"","sources":["../src/internal.ts"],"names":[],"mappings":"AAWA,YAAY,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAC3D,YAAY,EACV,gBAAgB,EAChB,kBAAkB,EAClB,YAAY,EACZ,iBAAiB,EACjB,mBAAmB,EACnB,iBAAiB,GAClB,MAAM,mBAAmB,CAAA;AAG1B,YAAY,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAGhE,OAAO,EACL,YAAY,EACZ,eAAe,EACf,gBAAgB,EAChB,cAAc,GACf,MAAM,qBAAqB,CAAA;AAG5B,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAG/F,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAG3E,YAAY,EACV,cAAc,EACd,aAAa,EACb,WAAW,EACX,WAAW,EACX,WAAW,EACX,UAAU,EACV,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,yBAAyB,GAC1B,MAAM,mBAAmB,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"relationship-options.d.ts","sourceRoot":"","sources":["../../src/query/relationship-options.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAExD,OAAO,EAA4B,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAI9E,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,uBAAuB;IACtC,sEAAsE;IACtE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,6DAA6D;IAC7D,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,2EAA2E;IAC3E,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;CACvB;AAED;;;;;;;;;GASG;AACH,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,kBAAkB,EAC3B,MAAM,EAAE,cAAc,EACtB,cAAc,EAAE,MAAM,EACtB,IAAI,GAAE,uBAA4B,GACjC,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAkC/B"}
1
+ {"version":3,"file":"relationship-options.d.ts","sourceRoot":"","sources":["../../src/query/relationship-options.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAExD,OAAO,EAA4B,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAI9E,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,uBAAuB;IACtC,sEAAsE;IACtE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,6DAA6D;IAC7D,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,2EAA2E;IAC3E,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;CACvB;AAED;;;;;;;;;GASG;AACH,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,kBAAkB,EAC3B,MAAM,EAAE,cAAc,EACtB,cAAc,EAAE,MAAM,EACtB,IAAI,GAAE,uBAA4B,GACjC,OAAO,CAAC,kBAAkB,EAAE,CAAC,CA0C/B"}
@@ -23,9 +23,15 @@ export async function getRelationshipOptions(context, config, relatedListKey, ar
23
23
  const { search, take = DEFAULT_TAKE, selectedIds = [] } = args;
24
24
  const labelFieldConfig = relatedListConfig.fields[labelField];
25
25
  const where = search && labelFieldConfig?.type === 'text' ? { [labelField]: { contains: search } } : undefined;
26
+ // Virtual/computed label fields (resolved at read time via `resolveOutput`)
27
+ // have no backing database column, so passing them into `orderBy` fails
28
+ // Prisma validation and 500s the request. Fall back to ordering by `id` —
29
+ // always a real, orderable column — whenever the label field is virtual.
30
+ const isVirtualLabel = labelFieldConfig?.type === 'virtual' || labelFieldConfig?.virtual === true;
31
+ const orderBy = isVirtualLabel ? { id: 'asc' } : { [labelField]: 'asc' };
26
32
  const primary = await runQuery(context, relatedListKey, fragment, {
27
33
  where,
28
- orderBy: { [labelField]: 'asc' },
34
+ orderBy,
29
35
  take,
30
36
  });
31
37
  const seenIds = new Set(primary.map((item) => item.id));
@@ -1 +1 @@
1
- {"version":3,"file":"relationship-options.js","sourceRoot":"","sources":["../../src/query/relationship-options.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACpE,OAAO,EAAE,cAAc,EAAE,QAAQ,EAA2B,MAAM,YAAY,CAAA;AAE9E,MAAM,YAAY,GAAG,EAAE,CAAA;AAgBvB;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,OAA2B,EAC3B,MAAsB,EACtB,cAAsB,EACtB,IAAI,GAA4B,EAAE;IAElC,MAAM,iBAAiB,GAAG,MAAM,CAAC,KAAK,CAAC,cAAc,CAAC,CAAA;IACtD,IAAI,CAAC,iBAAiB;QAAE,OAAO,EAAE,CAAA;IAEjC,MAAM,UAAU,GAAG,iBAAiB,CAAC,iBAAiB,CAAC,CAAA;IACvD,MAAM,QAAQ,GAAG,cAAc,EAA2B,CAAC;QACzD,EAAE,EAAE,IAAI;QACR,CAAC,UAAU,CAAC,EAAE,IAAI;KACnB,CAAC,CAAA;IAEF,MAAM,EAAE,MAAM,EAAE,IAAI,GAAG,YAAY,EAAE,WAAW,GAAG,EAAE,EAAE,GAAG,IAAI,CAAA;IAC9D,MAAM,gBAAgB,GAAG,iBAAiB,CAAC,MAAM,CAAC,UAAU,CAAkC,CAAA;IAC9F,MAAM,KAAK,GACT,MAAM,IAAI,gBAAgB,EAAE,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,CAAA;IAElG,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE;QAChE,KAAK;QACL,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,EAAE,KAAK,EAAE;QAChC,IAAI;KACL,CAAC,CAAA;IAEF,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAE,IAAuB,CAAC,EAAE,CAAC,CAAC,CAAA;IAC3E,MAAM,kBAAkB,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAA;IAEvE,MAAM,QAAQ,GAAG,kBAAkB,CAAC,MAAM;QACxC,CAAC,CAAC,MAAM,QAAQ,CAAC,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE;YAChD,KAAK,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,kBAAkB,EAAE,EAAE;SAC1C,CAAC;QACJ,CAAC,CAAC,EAAE,CAAA;IAEN,OAAO,CAAC,GAAG,OAAO,EAAE,GAAG,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QAC9C,EAAE,EAAG,IAAuB,CAAC,EAAE;QAC/B,KAAK,EAAE,YAAY,CAAC,iBAAiB,EAAE,IAA+B,CAAC;KACxE,CAAC,CAAC,CAAA;AACL,CAAC"}
1
+ {"version":3,"file":"relationship-options.js","sourceRoot":"","sources":["../../src/query/relationship-options.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACpE,OAAO,EAAE,cAAc,EAAE,QAAQ,EAA2B,MAAM,YAAY,CAAA;AAE9E,MAAM,YAAY,GAAG,EAAE,CAAA;AAgBvB;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,OAA2B,EAC3B,MAAsB,EACtB,cAAsB,EACtB,IAAI,GAA4B,EAAE;IAElC,MAAM,iBAAiB,GAAG,MAAM,CAAC,KAAK,CAAC,cAAc,CAAC,CAAA;IACtD,IAAI,CAAC,iBAAiB;QAAE,OAAO,EAAE,CAAA;IAEjC,MAAM,UAAU,GAAG,iBAAiB,CAAC,iBAAiB,CAAC,CAAA;IACvD,MAAM,QAAQ,GAAG,cAAc,EAA2B,CAAC;QACzD,EAAE,EAAE,IAAI;QACR,CAAC,UAAU,CAAC,EAAE,IAAI;KACnB,CAAC,CAAA;IAEF,MAAM,EAAE,MAAM,EAAE,IAAI,GAAG,YAAY,EAAE,WAAW,GAAG,EAAE,EAAE,GAAG,IAAI,CAAA;IAC9D,MAAM,gBAAgB,GAAG,iBAAiB,CAAC,MAAM,CAAC,UAAU,CACV,CAAA;IAClD,MAAM,KAAK,GACT,MAAM,IAAI,gBAAgB,EAAE,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,CAAA;IAElG,4EAA4E;IAC5E,wEAAwE;IACxE,0EAA0E;IAC1E,yEAAyE;IACzE,MAAM,cAAc,GAAG,gBAAgB,EAAE,IAAI,KAAK,SAAS,IAAI,gBAAgB,EAAE,OAAO,KAAK,IAAI,CAAA;IACjG,MAAM,OAAO,GAA0B,cAAc,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,EAAE,KAAK,EAAE,CAAA;IAE/F,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE;QAChE,KAAK;QACL,OAAO;QACP,IAAI;KACL,CAAC,CAAA;IAEF,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAE,IAAuB,CAAC,EAAE,CAAC,CAAC,CAAA;IAC3E,MAAM,kBAAkB,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAA;IAEvE,MAAM,QAAQ,GAAG,kBAAkB,CAAC,MAAM;QACxC,CAAC,CAAC,MAAM,QAAQ,CAAC,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE;YAChD,KAAK,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,kBAAkB,EAAE,EAAE;SAC1C,CAAC;QACJ,CAAC,CAAC,EAAE,CAAA;IAEN,OAAO,CAAC,GAAG,OAAO,EAAE,GAAG,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QAC9C,EAAE,EAAG,IAAuB,CAAC,EAAE;QAC/B,KAAK,EAAE,YAAY,CAAC,iBAAiB,EAAE,IAA+B,CAAC;KACxE,CAAC,CAAC,CAAA;AACL,CAAC"}
@@ -31,6 +31,13 @@ function makeConfig() {
31
31
  ui: { labelField: 'rank' },
32
32
  access: { operation: { query: () => true } },
33
33
  },
34
+ VirtualLabel: {
35
+ fields: {
36
+ displayName: { type: 'virtual', virtual: true },
37
+ },
38
+ ui: { labelField: 'displayName' },
39
+ access: { operation: { query: () => true } },
40
+ },
34
41
  },
35
42
  };
36
43
  }
@@ -75,6 +82,28 @@ describe('getRelationshipOptions', () => {
75
82
  expect(call.where).toBeUndefined();
76
83
  expect(call.orderBy).toEqual({ rank: 'asc' });
77
84
  });
85
+ it('falls back to ordering by id when the label field is virtual (no backing column)', async () => {
86
+ const rows = [{ id: 'v1', displayName: 'Computed One' }];
87
+ const delegate = makeDelegate(rows);
88
+ const context = makeContext({ virtualLabel: delegate });
89
+ const config = makeConfig();
90
+ const result = await getRelationshipOptions(context, config, 'VirtualLabel', { search: 'One' });
91
+ const call = delegate.findMany.mock.calls[0][0];
92
+ // Virtual label fields have no backing column, so ordering by them would
93
+ // 500 in Prisma — order by id instead, and skip the text `contains` filter.
94
+ expect(call.orderBy).toEqual({ id: 'asc' });
95
+ expect(call.where).toBeUndefined();
96
+ expect(result).toEqual([{ id: 'v1', label: 'Computed One' }]);
97
+ });
98
+ it('treats a field flagged virtual via type alone as non-orderable (orders by id)', async () => {
99
+ const config = makeConfig();
100
+ config.lists.VirtualLabel.fields.displayName.virtual = undefined;
101
+ const delegate = makeDelegate([{ id: 'v1', displayName: 'Computed One' }]);
102
+ const context = makeContext({ virtualLabel: delegate });
103
+ await getRelationshipOptions(context, config, 'VirtualLabel', { search: 'One' });
104
+ const call = delegate.findMany.mock.calls[0][0];
105
+ expect(call.orderBy).toEqual({ id: 'asc' });
106
+ });
78
107
  it('unions currently-selected ids even when beyond take / not matching search', async () => {
79
108
  // The bounded/search-scoped query only returns a1 (mimicking take:1 + search).
80
109
  const primaryDelegate = makeDelegate([authors[0]]);
@@ -1 +1 @@
1
- {"version":3,"file":"relationship-options.test.js","sourceRoot":"","sources":["../../src/query/relationship-options.test.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"relationship-options.test.js","sourceRoot":"","sources":["../../src/query/relationship-options.test.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opensaas/stack-core",
3
- "version": "0.27.1",
3
+ "version": "0.29.0",
4
4
  "description": "Core stack for OpenSaas - schema definition, access control, and runtime utilities",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -53,7 +53,7 @@
53
53
  },
54
54
  "dependencies": {
55
55
  "bcryptjs": "^3.0.3",
56
- "zod": "^4.3.6"
56
+ "zod": "^4.4.3"
57
57
  },
58
58
  "devDependencies": {
59
59
  "@prisma/client": "^7.8.0",
@@ -153,6 +153,8 @@ export type {
153
153
  ThemeConfig,
154
154
  ThemePreset,
155
155
  ThemeColors,
156
+ ThemeFonts,
157
+ ThemeShadows,
156
158
  McpConfig,
157
159
  McpToolsConfig,
158
160
  McpAuthConfig,
@@ -4,7 +4,6 @@ import type {
4
4
  PluginContext,
5
5
  ListConfig,
6
6
  Hooks,
7
- OperationAccess,
8
7
  McpCustomTool,
9
8
  BaseFieldConfig,
10
9
  } from './types.js'
@@ -125,29 +124,6 @@ function mergeHooks(existing: Hooks | undefined, extension: Hooks | undefined):
125
124
  return Object.keys(merged).length > 0 ? (merged as Hooks) : undefined
126
125
  }
127
126
 
128
- /**
129
- * Merge access control from extension into existing access
130
- */
131
- function mergeAccess(
132
- existing: { operation?: OperationAccess } | undefined,
133
- extension: { operation?: OperationAccess } | undefined,
134
- ): { operation?: OperationAccess } | undefined {
135
- if (!extension) return existing
136
- if (!existing) return extension
137
-
138
- const merged: { operation?: OperationAccess } = {}
139
-
140
- // Merge operation access (use extension if provided, otherwise keep existing)
141
- if (existing.operation || extension.operation) {
142
- merged.operation = {
143
- ...existing.operation,
144
- ...extension.operation,
145
- }
146
- }
147
-
148
- return merged
149
- }
150
-
151
127
  /**
152
128
  * Execute plugins and transform config
153
129
  * Returns modified config with plugin data attached
@@ -197,6 +173,19 @@ export async function executePlugins(config: OpenSaasConfig): Promise<OpenSaasCo
197
173
  )
198
174
  }
199
175
 
176
+ // Operation-level access belongs to whoever owns the list (the
177
+ // application, or an earlier plugin that created it via addList).
178
+ // An extension of a pre-existing list must never define or override
179
+ // that access — see ADR-0013. Fields/hooks/relationships/mcp may
180
+ // still be merged in below.
181
+ if (extension.access?.operation) {
182
+ throw new Error(
183
+ `Plugin "${plugin.name}" tried to set operation-level access while extending list "${name}", ` +
184
+ `but access control belongs to the application (or whichever party created the list), never a ` +
185
+ `plugin extending it. Remove "access" from this extension — see ADR-0013.`,
186
+ )
187
+ }
188
+
200
189
  // Deep merge fields
201
190
  const mergedFields = {
202
191
  ...existing.fields,
@@ -206,9 +195,6 @@ export async function executePlugins(config: OpenSaasConfig): Promise<OpenSaasCo
206
195
  // Merge hooks
207
196
  const mergedHooks = mergeHooks(existing.hooks, extension.hooks)
208
197
 
209
- // Merge access control
210
- const mergedAccess = mergeAccess(existing.access, extension.access)
211
-
212
198
  // Merge MCP config
213
199
  const mergedMcp = extension.mcp
214
200
  ? {
@@ -221,7 +207,6 @@ export async function executePlugins(config: OpenSaasConfig): Promise<OpenSaasCo
221
207
  ...existing,
222
208
  fields: mergedFields,
223
209
  hooks: mergedHooks,
224
- access: mergedAccess,
225
210
  mcp: mergedMcp,
226
211
  }
227
212
  },
@@ -593,6 +593,21 @@ export type BaseFieldConfig<TTypeInfo extends TypeInfo> = {
593
593
  * e.g., "color" to use a globally registered ColorPickerField
594
594
  */
595
595
  fieldType?: string
596
+ /**
597
+ * Help / description text rendered beneath the field control in the admin UI.
598
+ *
599
+ * `FieldRenderer` surfaces this to the rendered field component as its
600
+ * `helpText` prop, so it appears via the shared field-shell `FieldHelp`
601
+ * rhythm (data-slot="field-help") for admin-rendered fields.
602
+ *
603
+ * @example
604
+ * ```typescript
605
+ * fields: {
606
+ * slug: text({ ui: { description: 'URL-friendly identifier, lowercase only.' } })
607
+ * }
608
+ * ```
609
+ */
610
+ description?: string
596
611
  /**
597
612
  * Transform field value before sending to client (browser)
598
613
  * Useful for sensitive fields (e.g., passwords) or complex data structures
@@ -2029,8 +2044,13 @@ export type SessionConfig = {
2029
2044
  export type ThemePreset = 'modern' | 'classic' | 'neon'
2030
2045
 
2031
2046
  /**
2032
- * Custom theme colors (HSL values without hsl() wrapper)
2033
- * Format: "220 20% 97%" (hue saturation lightness)
2047
+ * Custom theme colors.
2048
+ *
2049
+ * Each value is passed through verbatim to the corresponding CSS custom
2050
+ * property, so any valid CSS color string works: `oklch(…)`, `#hex`, `rgb(…)`,
2051
+ * or a wrapped `hsl(…)`. Bare HSL triplets (`"220 20% 97%"`) are a clean break —
2052
+ * they are no longer accepted and trigger a dev-mode warning. See
2053
+ * `specs/THEMING.md` and ADR-0015.
2034
2054
  */
2035
2055
  export type ThemeColors = {
2036
2056
  background?: string
@@ -2049,6 +2069,10 @@ export type ThemeColors = {
2049
2069
  accentForeground?: string
2050
2070
  destructive?: string
2051
2071
  destructiveForeground?: string
2072
+ success?: string
2073
+ successForeground?: string
2074
+ warning?: string
2075
+ warningForeground?: string
2052
2076
  border?: string
2053
2077
  input?: string
2054
2078
  ring?: string
@@ -2057,27 +2081,59 @@ export type ThemeColors = {
2057
2081
  }
2058
2082
 
2059
2083
  /**
2060
- * Theme configuration
2084
+ * Font family tokens. Each value is a CSS `font-family` string, designed to
2085
+ * compose with `next/font`: set the value to the font's CSS variable, e.g.
2086
+ * `sans: 'var(--font-inter), system-ui, sans-serif'`. `heading` defaults to the
2087
+ * `sans` value when omitted.
2088
+ */
2089
+ export type ThemeFonts = {
2090
+ sans?: string
2091
+ mono?: string
2092
+ heading?: string
2093
+ }
2094
+
2095
+ /**
2096
+ * Elevation shadow tokens. Each value is a CSS `box-shadow` string. Set them to
2097
+ * `'none'` for a fully flat theme without forking components.
2098
+ */
2099
+ export type ThemeShadows = {
2100
+ sm?: string
2101
+ md?: string
2102
+ lg?: string
2103
+ }
2104
+
2105
+ /**
2106
+ * Theme configuration. Compiles to token overrides written onto the same CSS
2107
+ * custom properties the UI package stylesheet declares — the config layer and
2108
+ * the stylesheet write to one token set so they can never drift (ADR-0015).
2061
2109
  */
2062
2110
  export type ThemeConfig = {
2063
2111
  /**
2064
- * Preset theme to use
2112
+ * Preset theme to start from. Individual tokens can be overridden on top.
2065
2113
  * @default "modern"
2066
2114
  */
2067
2115
  preset?: ThemePreset
2068
2116
  /**
2069
- * Custom color overrides for light mode
2117
+ * Custom color overrides for light mode.
2070
2118
  */
2071
2119
  colors?: ThemeColors
2072
2120
  /**
2073
- * Custom color overrides for dark mode
2121
+ * Custom color overrides for dark mode.
2074
2122
  */
2075
2123
  darkColors?: ThemeColors
2076
2124
  /**
2077
- * Border radius in rem
2078
- * @default 0.75
2125
+ * Font family overrides (`--font-sans`, `--font-mono`, `--font-heading`).
2126
+ */
2127
+ fonts?: ThemeFonts
2128
+ /**
2129
+ * Base border radius in rem. Derived sm/md/lg radii are computed from it.
2130
+ * @default 0.625
2079
2131
  */
2080
2132
  radius?: number
2133
+ /**
2134
+ * Elevation shadow overrides (`--shadow-sm`, `--shadow-md`, `--shadow-lg`).
2135
+ */
2136
+ shadows?: ThemeShadows
2081
2137
  }
2082
2138
 
2083
2139
  /**
@@ -2437,8 +2493,21 @@ export type Plugin = {
2437
2493
  * Optional: Provide runtime services
2438
2494
  * Called when creating context to provide plugin-specific services
2439
2495
  * Return value is stored in context.plugins[pluginName]
2440
- */
2441
- runtime?: (context: import('../access/types.js').AccessContext) => unknown
2496
+ *
2497
+ * `sudo` returns an access-bypassing (but still hook-firing) `AccessContext`
2498
+ * for the same request — use `sudo().db` for reads/writes that must not
2499
+ * depend on the caller's own list access policy (e.g. an identity lookup
2500
+ * like "who is this session"). Deliberately NOT a method on `AccessContext`
2501
+ * itself — a self-referential `sudo(): AccessContext` field on that shared,
2502
+ * widely-instantiated interface tripped up TypeScript's structural checking
2503
+ * of unrelated generated Prisma types elsewhere (nullable JSON `CreateInput`
2504
+ * fields) in a downstream app; passing it as a plain second argument avoids
2505
+ * that recursion entirely.
2506
+ */
2507
+ runtime?: (
2508
+ context: import('../access/types.js').AccessContext,
2509
+ sudo: () => import('../access/types.js').AccessContext,
2510
+ ) => unknown
2442
2511
 
2443
2512
  /**
2444
2513
  * Optional: Type metadata for runtime services
@@ -393,7 +393,12 @@ export function getContext<
393
393
  for (const plugin of pluginsToExecute) {
394
394
  if (plugin.runtime) {
395
395
  try {
396
- context.plugins[plugin.name] = plugin.runtime(context)
396
+ // Passed as a plain second argument rather than a method on
397
+ // `context` itself — see the `sudo` param doc on `Plugin['runtime']`.
398
+ context.plugins[plugin.name] = plugin.runtime(
399
+ context,
400
+ () => sudo() as unknown as AccessContext<TPrisma>,
401
+ )
397
402
  } catch (error) {
398
403
  console.error(`Error executing runtime for plugin "${plugin.name}":`, error)
399
404
  // Continue with other plugins even if one fails
package/src/internal.ts CHANGED
@@ -43,6 +43,8 @@ export type {
43
43
  ThemeConfig,
44
44
  ThemePreset,
45
45
  ThemeColors,
46
+ ThemeFonts,
47
+ ThemeShadows,
46
48
  FileMetadata,
47
49
  ImageMetadata,
48
50
  ImageTransformationResult,
@@ -39,6 +39,13 @@ function makeConfig(): OpenSaasConfig {
39
39
  ui: { labelField: 'rank' },
40
40
  access: { operation: { query: () => true } },
41
41
  },
42
+ VirtualLabel: {
43
+ fields: {
44
+ displayName: { type: 'virtual', virtual: true },
45
+ },
46
+ ui: { labelField: 'displayName' },
47
+ access: { operation: { query: () => true } },
48
+ },
42
49
  },
43
50
  } as unknown as OpenSaasConfig
44
51
  }
@@ -99,6 +106,36 @@ describe('getRelationshipOptions', () => {
99
106
  expect(call.orderBy).toEqual({ rank: 'asc' })
100
107
  })
101
108
 
109
+ it('falls back to ordering by id when the label field is virtual (no backing column)', async () => {
110
+ const rows = [{ id: 'v1', displayName: 'Computed One' }]
111
+ const delegate = makeDelegate(rows)
112
+ const context = makeContext({ virtualLabel: delegate })
113
+ const config = makeConfig()
114
+
115
+ const result = await getRelationshipOptions(context, config, 'VirtualLabel', { search: 'One' })
116
+
117
+ const call = delegate.findMany.mock.calls[0][0] as Record<string, unknown>
118
+ // Virtual label fields have no backing column, so ordering by them would
119
+ // 500 in Prisma — order by id instead, and skip the text `contains` filter.
120
+ expect(call.orderBy).toEqual({ id: 'asc' })
121
+ expect(call.where).toBeUndefined()
122
+ expect(result).toEqual([{ id: 'v1', label: 'Computed One' }])
123
+ })
124
+
125
+ it('treats a field flagged virtual via type alone as non-orderable (orders by id)', async () => {
126
+ const config = makeConfig()
127
+ // Only `type: 'virtual'` is set here (no `virtual: true`) to lock in the
128
+ // discriminator against future refactors.
129
+ ;(config.lists.VirtualLabel.fields.displayName as { virtual?: boolean }).virtual = undefined
130
+ const delegate = makeDelegate([{ id: 'v1', displayName: 'Computed One' }])
131
+ const context = makeContext({ virtualLabel: delegate })
132
+
133
+ await getRelationshipOptions(context, config, 'VirtualLabel', { search: 'One' })
134
+
135
+ const call = delegate.findMany.mock.calls[0][0] as Record<string, unknown>
136
+ expect(call.orderBy).toEqual({ id: 'asc' })
137
+ })
138
+
102
139
  it('unions currently-selected ids even when beyond take / not matching search', async () => {
103
140
  // The bounded/search-scoped query only returns a1 (mimicking take:1 + search).
104
141
  const primaryDelegate = makeDelegate([authors[0]])
@@ -44,13 +44,21 @@ export async function getRelationshipOptions(
44
44
  })
45
45
 
46
46
  const { search, take = DEFAULT_TAKE, selectedIds = [] } = args
47
- const labelFieldConfig = relatedListConfig.fields[labelField] as { type?: string } | undefined
47
+ const labelFieldConfig = relatedListConfig.fields[labelField] as
48
+ { type?: string; virtual?: boolean } | undefined
48
49
  const where =
49
50
  search && labelFieldConfig?.type === 'text' ? { [labelField]: { contains: search } } : undefined
50
51
 
52
+ // Virtual/computed label fields (resolved at read time via `resolveOutput`)
53
+ // have no backing database column, so passing them into `orderBy` fails
54
+ // Prisma validation and 500s the request. Fall back to ordering by `id` —
55
+ // always a real, orderable column — whenever the label field is virtual.
56
+ const isVirtualLabel = labelFieldConfig?.type === 'virtual' || labelFieldConfig?.virtual === true
57
+ const orderBy: Record<string, 'asc'> = isVirtualLabel ? { id: 'asc' } : { [labelField]: 'asc' }
58
+
51
59
  const primary = await runQuery(context, relatedListKey, fragment, {
52
60
  where,
53
- orderBy: { [labelField]: 'asc' },
61
+ orderBy,
54
62
  take,
55
63
  })
56
64
 
@@ -681,8 +681,8 @@ describe('Plugin Engine', () => {
681
681
  })
682
682
  })
683
683
 
684
- describe('access control merging', () => {
685
- test('merges access control from plugins', async () => {
684
+ describe('access control guardrail (ADR-0013)', () => {
685
+ test('throws when an extension carries operation access for a pre-existing list', async () => {
686
686
  const plugin: Plugin = {
687
687
  name: 'test-plugin',
688
688
  init: async (context) => {
@@ -711,23 +711,19 @@ describe('Plugin Engine', () => {
711
711
  plugins: [plugin],
712
712
  }
713
713
 
714
- const result = await executePlugins(config)
715
-
716
- expect(result.lists.Post.access?.operation?.query).toBeDefined()
717
- expect(result.lists.Post.access?.operation?.create).toBeDefined()
718
- expect(result.lists.Post.access?.operation?.update).toBeDefined()
714
+ await expect(executePlugins(config)).rejects.toThrow(
715
+ 'Plugin "test-plugin" tried to set operation-level access while extending list "Post"',
716
+ )
719
717
  })
720
718
 
721
- test('plugin access control overrides existing', async () => {
722
- const pluginQuery = vi.fn(() => false)
723
-
719
+ test('extension attempting to override existing access throws instead of silently winning', async () => {
724
720
  const plugin: Plugin = {
725
721
  name: 'test-plugin',
726
722
  init: async (context) => {
727
723
  context.extendList('Post', {
728
724
  access: {
729
725
  operation: {
730
- query: pluginQuery,
726
+ query: () => false,
731
727
  },
732
728
  },
733
729
  })
@@ -750,10 +746,67 @@ describe('Plugin Engine', () => {
750
746
  plugins: [plugin],
751
747
  }
752
748
 
749
+ await expect(executePlugins(config)).rejects.toThrow('Post')
750
+
751
+ // The host's access is untouched — the throw happens before any mutation.
752
+ expect(config.lists.Post.access?.operation?.query).toBe(originalQuery)
753
+ })
754
+
755
+ test('extension providing only fields/hooks for a pre-existing list leaves its access untouched', async () => {
756
+ const originalQuery = vi.fn(() => true)
757
+
758
+ const plugin: Plugin = {
759
+ name: 'test-plugin',
760
+ init: async (context) => {
761
+ context.extendList('Post', {
762
+ fields: { views: integer() },
763
+ })
764
+ },
765
+ }
766
+
767
+ const config: OpenSaasConfig = {
768
+ lists: {
769
+ Post: {
770
+ fields: { title: text() },
771
+ access: {
772
+ operation: {
773
+ query: originalQuery,
774
+ },
775
+ },
776
+ },
777
+ },
778
+ plugins: [plugin],
779
+ }
780
+
781
+ const result = await executePlugins(config)
782
+
783
+ expect(result.lists.Post.fields.views).toBeDefined()
784
+ expect(result.lists.Post.access?.operation?.query).toBe(originalQuery)
785
+ })
786
+
787
+ test('a plugin creating a new list may still set its access via addList', async () => {
788
+ const plugin: Plugin = {
789
+ name: 'test-plugin',
790
+ init: async (context) => {
791
+ context.addList('Widget', {
792
+ fields: { name: text() },
793
+ access: {
794
+ operation: {
795
+ query: () => true,
796
+ },
797
+ },
798
+ })
799
+ },
800
+ }
801
+
802
+ const config: OpenSaasConfig = {
803
+ lists: {},
804
+ plugins: [plugin],
805
+ }
806
+
753
807
  const result = await executePlugins(config)
754
808
 
755
- // Plugin's access control should override original
756
- expect(result.lists.Post.access?.operation?.query).toBe(pluginQuery)
809
+ expect(result.lists.Widget.access?.operation?.query).toBeDefined()
757
810
  })
758
811
  })
759
812
 
@@ -3,6 +3,8 @@ import { getContext } from '../src/context/index.js'
3
3
  import { config, list } from '../src/config/index.js'
4
4
  import { text, integer, relationship } from '../src/fields/index.js'
5
5
  import type { PrismaClient } from '@prisma/client'
6
+ import type { Plugin } from '../src/config/types.js'
7
+ import type { AccessContext } from '../src/access/types.js'
6
8
 
7
9
  describe('Sudo Context', () => {
8
10
  // Mock Prisma client
@@ -624,4 +626,51 @@ describe('Sudo Context', () => {
624
626
  ).rejects.toThrow('Access denied')
625
627
  })
626
628
  })
629
+
630
+ describe('Plugin runtime sudo access', () => {
631
+ // A plugin's `runtime(context, sudo)` factory receives a `sudo` helper as a
632
+ // plain second argument — NOT a method on `AccessContext` itself. A
633
+ // self-referential `sudo(): AccessContext` field on that shared, widely
634
+ // instantiated interface was found to break TypeScript's structural
635
+ // checking of unrelated generated Prisma types in a downstream app
636
+ // (nullable JSON `CreateInput` fields); passing it as a separate argument
637
+ // avoids that recursion while still giving plugins (e.g. the auth
638
+ // plugin's getUser/getCurrentUser, see ADR-0013) an access-bypassing
639
+ // identity-lookup path.
640
+ it('passes a working sudo() as the second argument to plugin.runtime()', async () => {
641
+ let capturedSudo: (() => AccessContext<typeof mockPrisma>) | undefined
642
+
643
+ const plugin: Plugin = {
644
+ name: 'test-plugin',
645
+ init: async () => {},
646
+ runtime: (_context, sudo) => {
647
+ capturedSudo = sudo as () => AccessContext<typeof mockPrisma>
648
+ return {}
649
+ },
650
+ }
651
+
652
+ const pluginConfig = await config({
653
+ db: { provider: 'sqlite' },
654
+ plugins: [plugin],
655
+ lists: {
656
+ Post: list({
657
+ fields: { title: text({ validation: { isRequired: true } }) },
658
+ // Closed list: only sudo() should be able to read from it.
659
+ access: { operation: { query: () => false } },
660
+ }),
661
+ },
662
+ })
663
+
664
+ mockPrisma.post.findMany.mockResolvedValue([{ id: '1', title: 'Test Post' }])
665
+
666
+ getContext(pluginConfig, mockPrisma, null)
667
+
668
+ expect(capturedSudo).toBeTypeOf('function')
669
+
670
+ const sudoContext = capturedSudo!()
671
+ const sudoResult = await sudoContext.db.post.findMany()
672
+ expect(sudoResult).toHaveLength(1)
673
+ expect(sudoResult[0].title).toBe('Test Post')
674
+ })
675
+ })
627
676
  })