@btst/stack 2.11.3 → 2.11.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dist/packages/stack/src/plugins/cms/api/mutations.cjs +12 -1
  2. package/dist/packages/stack/src/plugins/cms/api/mutations.mjs +12 -1
  3. package/dist/packages/stack/src/plugins/cms/api/plugin.cjs +11 -51
  4. package/dist/packages/stack/src/plugins/cms/api/plugin.mjs +3 -43
  5. package/dist/packages/stack/src/plugins/cms/api/relations.cjs +76 -0
  6. package/dist/packages/stack/src/plugins/cms/api/relations.mjs +70 -0
  7. package/dist/packages/stack/src/plugins/cms/client/components/forms/content-form.cjs +58 -34
  8. package/dist/packages/stack/src/plugins/cms/client/components/forms/content-form.mjs +58 -34
  9. package/dist/packages/stack/src/plugins/cms/client/components/inverse-relations-panel.cjs +29 -7
  10. package/dist/packages/stack/src/plugins/cms/client/components/inverse-relations-panel.mjs +29 -7
  11. package/dist/packages/ui/src/components/auto-form/helpers.cjs +24 -1
  12. package/dist/packages/ui/src/components/auto-form/helpers.mjs +24 -1
  13. package/dist/plugins/blog/api/index.d.cts +3 -2
  14. package/dist/plugins/blog/api/index.d.mts +3 -2
  15. package/dist/plugins/blog/api/index.d.ts +3 -2
  16. package/dist/plugins/blog/client/components/index.cjs +21 -0
  17. package/dist/plugins/blog/client/components/index.d.cts +71 -1
  18. package/dist/plugins/blog/client/components/index.d.mts +71 -1
  19. package/dist/plugins/blog/client/components/index.d.ts +71 -1
  20. package/dist/plugins/blog/client/components/index.mjs +15 -1
  21. package/dist/plugins/blog/client/hooks/index.d.cts +3 -2
  22. package/dist/plugins/blog/client/hooks/index.d.mts +3 -2
  23. package/dist/plugins/blog/client/hooks/index.d.ts +3 -2
  24. package/dist/plugins/blog/client/index.d.cts +3 -2
  25. package/dist/plugins/blog/client/index.d.mts +3 -2
  26. package/dist/plugins/blog/client/index.d.ts +3 -2
  27. package/dist/plugins/blog/query-keys.d.cts +3 -2
  28. package/dist/plugins/blog/query-keys.d.mts +3 -2
  29. package/dist/plugins/blog/query-keys.d.ts +3 -2
  30. package/dist/plugins/cms/api/index.d.cts +1 -1
  31. package/dist/plugins/cms/api/index.d.mts +1 -1
  32. package/dist/plugins/cms/api/index.d.ts +1 -1
  33. package/dist/plugins/cms/query-keys.d.cts +1 -1
  34. package/dist/plugins/cms/query-keys.d.mts +1 -1
  35. package/dist/plugins/cms/query-keys.d.ts +1 -1
  36. package/dist/plugins/kanban/api/index.d.cts +1 -1
  37. package/dist/plugins/kanban/api/index.d.mts +1 -1
  38. package/dist/plugins/kanban/api/index.d.ts +1 -1
  39. package/dist/plugins/kanban/query-keys.d.cts +1 -1
  40. package/dist/plugins/kanban/query-keys.d.mts +1 -1
  41. package/dist/plugins/kanban/query-keys.d.ts +1 -1
  42. package/dist/shared/{stack.DGsFF5qb.d.cts → stack.BnSAhKJi.d.ts} +6 -5
  43. package/dist/shared/{stack.CntKf20s.d.cts → stack.CLWhMntf.d.cts} +12 -11
  44. package/dist/shared/{stack.Sod_PZhB.d.cts → stack.CuhcFsAx.d.cts} +7 -42
  45. package/dist/shared/{stack.Sod_PZhB.d.mts → stack.CuhcFsAx.d.mts} +7 -42
  46. package/dist/shared/{stack.Sod_PZhB.d.ts → stack.CuhcFsAx.d.ts} +7 -42
  47. package/dist/shared/{stack.QrBE0Bc8.d.ts → stack.DNLuzDb3.d.cts} +6 -5
  48. package/dist/shared/{stack.BX7MHi0J.d.mts → stack.DbnCLdNF.d.mts} +34 -7
  49. package/dist/shared/{stack.NtflNnnH.d.mts → stack.Dfc44JiE.d.mts} +6 -5
  50. package/dist/shared/{stack.CLrGRIj0.d.mts → stack.DiodAFAa.d.mts} +12 -11
  51. package/dist/shared/stack.Dsatjh0B.d.cts +35 -0
  52. package/dist/shared/stack.Dsatjh0B.d.mts +35 -0
  53. package/dist/shared/stack.Dsatjh0B.d.ts +35 -0
  54. package/dist/shared/{stack.fQjVhw5a.d.ts → stack.fXlShnhv.d.ts} +34 -7
  55. package/dist/shared/{stack.C9zoS1TN.d.cts → stack.r6HohbyX.d.cts} +34 -7
  56. package/dist/shared/{stack.BQSy-NDc.d.ts → stack.ufcExQIR.d.ts} +12 -11
  57. package/package.json +14 -1
  58. package/src/plugins/blog/client/components/index.tsx +52 -0
  59. package/src/plugins/cms/__tests__/auto-form-nested-config.test.ts +177 -0
  60. package/src/plugins/cms/api/index.ts +1 -0
  61. package/src/plugins/cms/api/mutations.ts +48 -3
  62. package/src/plugins/cms/api/plugin.ts +11 -101
  63. package/src/plugins/cms/api/relations.ts +174 -0
  64. package/src/plugins/cms/client/components/forms/content-form.tsx +93 -47
  65. package/src/plugins/cms/client/components/inverse-relations-panel.tsx +49 -1
  66. package/dist/shared/{stack.CMbX8Q5C.d.ts → stack.D4Cea8II.d.ts} +13 -13
  67. package/dist/shared/{stack.Dj04W2c3.d.mts → stack.HE_IvqV5.d.mts} +13 -13
  68. package/dist/shared/{stack.eq5eg1yt.d.cts → stack.Rtcvl8sS.d.cts} +13 -13
@@ -1,11 +1,12 @@
1
1
  import * as _tanstack_react_query from '@tanstack/react-query';
2
2
  import { QueryClient } from '@tanstack/react-query';
3
3
  import { createApiClient } from '@btst/stack/plugins/client';
4
- import { P as Post, T as Tag, c as createPostSchema, u as updatePostSchema, S as SerializedPost, a as SerializedTag } from './stack.Sod_PZhB.mjs';
4
+ import { P as Post, T as Tag, S as SerializedPost, a as SerializedTag } from './stack.Dsatjh0B.mjs';
5
5
  import * as _btst_stack_plugins_api from '@btst/stack/plugins/api';
6
6
  import { DBAdapter } from '@btst/db';
7
7
  import * as better_call from 'better-call';
8
8
  import { z } from 'zod';
9
+ import { c as createPostSchema, u as updatePostSchema } from './stack.CuhcFsAx.mjs';
9
10
 
10
11
  /**
11
12
  * Parameters for filtering/paginating posts.
@@ -316,12 +317,6 @@ declare const blogBackendPlugin: (hooks?: BlogBackendHooks) => _btst_stack_plugi
316
317
  title: string;
317
318
  content: string;
318
319
  excerpt: string;
319
- slug?: string | undefined;
320
- published?: boolean | undefined;
321
- publishedAt?: unknown;
322
- createdAt?: unknown;
323
- updatedAt?: unknown;
324
- image?: string | undefined;
325
320
  tags?: ({
326
321
  name: string;
327
322
  } | {
@@ -329,16 +324,16 @@ declare const blogBackendPlugin: (hooks?: BlogBackendHooks) => _btst_stack_plugi
329
324
  name: string;
330
325
  slug: string;
331
326
  })[] | undefined;
332
- }, {
333
- title: string;
334
- content: string;
335
- excerpt: string;
336
327
  slug?: string | undefined;
337
328
  published?: boolean | undefined;
338
329
  publishedAt?: unknown;
339
330
  createdAt?: unknown;
340
331
  updatedAt?: unknown;
341
332
  image?: string | undefined;
333
+ }, {
334
+ title: string;
335
+ content: string;
336
+ excerpt: string;
342
337
  tags?: ({
343
338
  name: string;
344
339
  } | {
@@ -346,6 +341,12 @@ declare const blogBackendPlugin: (hooks?: BlogBackendHooks) => _btst_stack_plugi
346
341
  name: string;
347
342
  slug: string;
348
343
  })[] | undefined;
344
+ slug?: string | undefined;
345
+ published?: boolean | undefined;
346
+ publishedAt?: unknown;
347
+ createdAt?: unknown;
348
+ updatedAt?: unknown;
349
+ image?: string | undefined;
349
350
  }>;
350
351
  }, Post>;
351
352
  readonly updatePost: better_call.StrictEndpoint<"/posts/:id", {} & {
@@ -0,0 +1,35 @@
1
+ type Post = {
2
+ id: string;
3
+ authorId?: string;
4
+ defaultLocale?: string;
5
+ slug: string;
6
+ title: string;
7
+ content: string;
8
+ excerpt: string;
9
+ image?: string;
10
+ published: boolean;
11
+ status?: "DRAFT" | "PUBLISHED";
12
+ tags: Tag[];
13
+ publishedAt?: Date;
14
+ createdAt: Date;
15
+ updatedAt: Date;
16
+ };
17
+ type Tag = {
18
+ id: string;
19
+ slug: string;
20
+ name: string;
21
+ createdAt: Date;
22
+ updatedAt: Date;
23
+ };
24
+ interface SerializedPost extends Omit<Post, "createdAt" | "updatedAt" | "publishedAt" | "tags"> {
25
+ tags: SerializedTag[];
26
+ publishedAt?: string;
27
+ createdAt: string;
28
+ updatedAt: string;
29
+ }
30
+ interface SerializedTag extends Omit<Tag, "createdAt" | "updatedAt"> {
31
+ createdAt: string;
32
+ updatedAt: string;
33
+ }
34
+
35
+ export type { Post as P, SerializedPost as S, Tag as T, SerializedTag as a };
@@ -0,0 +1,35 @@
1
+ type Post = {
2
+ id: string;
3
+ authorId?: string;
4
+ defaultLocale?: string;
5
+ slug: string;
6
+ title: string;
7
+ content: string;
8
+ excerpt: string;
9
+ image?: string;
10
+ published: boolean;
11
+ status?: "DRAFT" | "PUBLISHED";
12
+ tags: Tag[];
13
+ publishedAt?: Date;
14
+ createdAt: Date;
15
+ updatedAt: Date;
16
+ };
17
+ type Tag = {
18
+ id: string;
19
+ slug: string;
20
+ name: string;
21
+ createdAt: Date;
22
+ updatedAt: Date;
23
+ };
24
+ interface SerializedPost extends Omit<Post, "createdAt" | "updatedAt" | "publishedAt" | "tags"> {
25
+ tags: SerializedTag[];
26
+ publishedAt?: string;
27
+ createdAt: string;
28
+ updatedAt: string;
29
+ }
30
+ interface SerializedTag extends Omit<Tag, "createdAt" | "updatedAt"> {
31
+ createdAt: string;
32
+ updatedAt: string;
33
+ }
34
+
35
+ export type { Post as P, SerializedPost as S, Tag as T, SerializedTag as a };
@@ -0,0 +1,35 @@
1
+ type Post = {
2
+ id: string;
3
+ authorId?: string;
4
+ defaultLocale?: string;
5
+ slug: string;
6
+ title: string;
7
+ content: string;
8
+ excerpt: string;
9
+ image?: string;
10
+ published: boolean;
11
+ status?: "DRAFT" | "PUBLISHED";
12
+ tags: Tag[];
13
+ publishedAt?: Date;
14
+ createdAt: Date;
15
+ updatedAt: Date;
16
+ };
17
+ type Tag = {
18
+ id: string;
19
+ slug: string;
20
+ name: string;
21
+ createdAt: Date;
22
+ updatedAt: Date;
23
+ };
24
+ interface SerializedPost extends Omit<Post, "createdAt" | "updatedAt" | "publishedAt" | "tags"> {
25
+ tags: SerializedTag[];
26
+ publishedAt?: string;
27
+ createdAt: string;
28
+ updatedAt: string;
29
+ }
30
+ interface SerializedTag extends Omit<Tag, "createdAt" | "updatedAt"> {
31
+ createdAt: string;
32
+ updatedAt: string;
33
+ }
34
+
35
+ export type { Post as P, SerializedPost as S, Tag as T, SerializedTag as a };
@@ -87,12 +87,36 @@ interface CreateCMSContentItemInput {
87
87
  /** Arbitrary data payload — should match the content type schema */
88
88
  data: Record<string, unknown>;
89
89
  }
90
+ /**
91
+ * Options for {@link createCMSContentItem}.
92
+ */
93
+ interface CreateCMSContentItemOptions {
94
+ /**
95
+ * When `true`, persist relation fields (`belongsTo`, `hasMany`,
96
+ * `manyToMany`) into the `contentRelation` junction table in addition to
97
+ * the item's JSON payload.
98
+ *
99
+ * This is what the HTTP `POST /content/:typeSlug` route does, and is
100
+ * required for the admin "Related Items" panel / inverse-relation queries
101
+ * to find the item.
102
+ *
103
+ * Seeds and other programmatic callers that want the admin UI to work
104
+ * should enable this flag. Callers are still expected to pass
105
+ * pre-created target IDs (`{ id }` or `[{ id }, ...]`) — inline `_new`
106
+ * creation is only supported via the HTTP route.
107
+ *
108
+ * Defaults to `false` for backwards compatibility (a no-op for content
109
+ * types without relations).
110
+ */
111
+ syncRelations?: boolean;
112
+ }
90
113
  /**
91
114
  * Create a new content item for a content type (looked up by slug).
92
115
  *
93
- * Bypasses Zod schema validation and relation processing — the caller is
94
- * responsible for providing valid, relation-free data. For relation fields or
95
- * schema validation, use the HTTP endpoint instead.
116
+ * Bypasses Zod schema validation and inline `_new` relation creation — the
117
+ * caller is responsible for providing valid data and pre-created relation
118
+ * IDs. For schema validation or inline creation of related items, use the
119
+ * HTTP endpoint instead.
96
120
  *
97
121
  * Throws if the content type is not found or the slug is already taken within
98
122
  * that content type.
@@ -104,8 +128,11 @@ interface CreateCMSContentItemInput {
104
128
  * @param adapter - The database adapter
105
129
  * @param contentTypeSlug - Slug of the target content type
106
130
  * @param input - Item slug and data payload
131
+ * @param options - See {@link CreateCMSContentItemOptions}. Pass
132
+ * `{ syncRelations: true }` to also populate the `contentRelation`
133
+ * junction table for relation fields in the payload.
107
134
  */
108
- declare function createCMSContentItem(adapter: DBAdapter, contentTypeSlug: string, input: CreateCMSContentItemInput): Promise<SerializedContentItem>;
135
+ declare function createCMSContentItem(adapter: DBAdapter, contentTypeSlug: string, input: CreateCMSContentItemInput, options?: CreateCMSContentItemOptions): Promise<SerializedContentItem>;
109
136
 
110
137
  /**
111
138
  * Route keys for the CMS plugin — matches the keys returned by
@@ -331,7 +358,7 @@ declare const cmsBackendPlugin: (config: CMSBackendConfig) => _btst_stack_plugin
331
358
  getContentItemBySlug: (contentTypeSlug: string, slug: string) => Promise<SerializedContentItemWithType<Record<string, unknown>> | null>;
332
359
  getContentItemById: (id: string) => Promise<SerializedContentItemWithType<Record<string, unknown>> | null>;
333
360
  prefetchForRoute: CMSPrefetchForRoute;
334
- createContentItem: (typeSlug: string, input: Parameters<typeof createCMSContentItem>[2]) => Promise<SerializedContentItem>;
361
+ createContentItem: (typeSlug: string, input: Parameters<typeof createCMSContentItem>[2], options?: Parameters<typeof createCMSContentItem>[3]) => Promise<SerializedContentItem>;
335
362
  }>;
336
363
  type CMSApiRouter = ReturnType<ReturnType<typeof cmsBackendPlugin>["routes"]>;
337
364
 
@@ -368,5 +395,5 @@ declare const CMS_QUERY_KEYS: {
368
395
  contentDetail: (typeSlug: string, id: string) => readonly ["cmsContent", "detail", string, string];
369
396
  };
370
397
 
371
- export { cmsBackendPlugin as c, getAllContentItems as d, getContentItemBySlug as e, getContentItemById as f, getAllContentTypes as g, serializeContentItem as h, serializeContentItemWithType as i, createCMSContentItem as j, CMS_QUERY_KEYS as l, serializeContentType as s };
372
- export type { CMSApiRouter as C, ContentListDiscriminator as a, CMSRouteKey as b, CreateCMSContentItemInput as k };
398
+ export { cmsBackendPlugin as c, getAllContentItems as d, getContentItemBySlug as e, getContentItemById as f, getAllContentTypes as g, serializeContentItem as h, serializeContentItemWithType as i, createCMSContentItem as j, CMS_QUERY_KEYS as m, serializeContentType as s };
399
+ export type { CMSApiRouter as C, ContentListDiscriminator as a, CMSRouteKey as b, CreateCMSContentItemInput as k, CreateCMSContentItemOptions as l };
@@ -87,12 +87,36 @@ interface CreateCMSContentItemInput {
87
87
  /** Arbitrary data payload — should match the content type schema */
88
88
  data: Record<string, unknown>;
89
89
  }
90
+ /**
91
+ * Options for {@link createCMSContentItem}.
92
+ */
93
+ interface CreateCMSContentItemOptions {
94
+ /**
95
+ * When `true`, persist relation fields (`belongsTo`, `hasMany`,
96
+ * `manyToMany`) into the `contentRelation` junction table in addition to
97
+ * the item's JSON payload.
98
+ *
99
+ * This is what the HTTP `POST /content/:typeSlug` route does, and is
100
+ * required for the admin "Related Items" panel / inverse-relation queries
101
+ * to find the item.
102
+ *
103
+ * Seeds and other programmatic callers that want the admin UI to work
104
+ * should enable this flag. Callers are still expected to pass
105
+ * pre-created target IDs (`{ id }` or `[{ id }, ...]`) — inline `_new`
106
+ * creation is only supported via the HTTP route.
107
+ *
108
+ * Defaults to `false` for backwards compatibility (a no-op for content
109
+ * types without relations).
110
+ */
111
+ syncRelations?: boolean;
112
+ }
90
113
  /**
91
114
  * Create a new content item for a content type (looked up by slug).
92
115
  *
93
- * Bypasses Zod schema validation and relation processing — the caller is
94
- * responsible for providing valid, relation-free data. For relation fields or
95
- * schema validation, use the HTTP endpoint instead.
116
+ * Bypasses Zod schema validation and inline `_new` relation creation — the
117
+ * caller is responsible for providing valid data and pre-created relation
118
+ * IDs. For schema validation or inline creation of related items, use the
119
+ * HTTP endpoint instead.
96
120
  *
97
121
  * Throws if the content type is not found or the slug is already taken within
98
122
  * that content type.
@@ -104,8 +128,11 @@ interface CreateCMSContentItemInput {
104
128
  * @param adapter - The database adapter
105
129
  * @param contentTypeSlug - Slug of the target content type
106
130
  * @param input - Item slug and data payload
131
+ * @param options - See {@link CreateCMSContentItemOptions}. Pass
132
+ * `{ syncRelations: true }` to also populate the `contentRelation`
133
+ * junction table for relation fields in the payload.
107
134
  */
108
- declare function createCMSContentItem(adapter: DBAdapter, contentTypeSlug: string, input: CreateCMSContentItemInput): Promise<SerializedContentItem>;
135
+ declare function createCMSContentItem(adapter: DBAdapter, contentTypeSlug: string, input: CreateCMSContentItemInput, options?: CreateCMSContentItemOptions): Promise<SerializedContentItem>;
109
136
 
110
137
  /**
111
138
  * Route keys for the CMS plugin — matches the keys returned by
@@ -331,7 +358,7 @@ declare const cmsBackendPlugin: (config: CMSBackendConfig) => _btst_stack_plugin
331
358
  getContentItemBySlug: (contentTypeSlug: string, slug: string) => Promise<SerializedContentItemWithType<Record<string, unknown>> | null>;
332
359
  getContentItemById: (id: string) => Promise<SerializedContentItemWithType<Record<string, unknown>> | null>;
333
360
  prefetchForRoute: CMSPrefetchForRoute;
334
- createContentItem: (typeSlug: string, input: Parameters<typeof createCMSContentItem>[2]) => Promise<SerializedContentItem>;
361
+ createContentItem: (typeSlug: string, input: Parameters<typeof createCMSContentItem>[2], options?: Parameters<typeof createCMSContentItem>[3]) => Promise<SerializedContentItem>;
335
362
  }>;
336
363
  type CMSApiRouter = ReturnType<ReturnType<typeof cmsBackendPlugin>["routes"]>;
337
364
 
@@ -368,5 +395,5 @@ declare const CMS_QUERY_KEYS: {
368
395
  contentDetail: (typeSlug: string, id: string) => readonly ["cmsContent", "detail", string, string];
369
396
  };
370
397
 
371
- export { cmsBackendPlugin as c, getAllContentItems as d, getContentItemBySlug as e, getContentItemById as f, getAllContentTypes as g, serializeContentItem as h, serializeContentItemWithType as i, createCMSContentItem as j, CMS_QUERY_KEYS as l, serializeContentType as s };
372
- export type { CMSApiRouter as C, ContentListDiscriminator as a, CMSRouteKey as b, CreateCMSContentItemInput as k };
398
+ export { cmsBackendPlugin as c, getAllContentItems as d, getContentItemBySlug as e, getContentItemById as f, getAllContentTypes as g, serializeContentItem as h, serializeContentItemWithType as i, createCMSContentItem as j, CMS_QUERY_KEYS as m, serializeContentType as s };
399
+ export type { CMSApiRouter as C, ContentListDiscriminator as a, CMSRouteKey as b, CreateCMSContentItemInput as k, CreateCMSContentItemOptions as l };
@@ -1,11 +1,12 @@
1
1
  import * as _tanstack_react_query from '@tanstack/react-query';
2
2
  import { QueryClient } from '@tanstack/react-query';
3
3
  import { createApiClient } from '@btst/stack/plugins/client';
4
- import { P as Post, T as Tag, c as createPostSchema, u as updatePostSchema, S as SerializedPost, a as SerializedTag } from './stack.Sod_PZhB.js';
4
+ import { P as Post, T as Tag, S as SerializedPost, a as SerializedTag } from './stack.Dsatjh0B.js';
5
5
  import * as _btst_stack_plugins_api from '@btst/stack/plugins/api';
6
6
  import { DBAdapter } from '@btst/db';
7
7
  import * as better_call from 'better-call';
8
8
  import { z } from 'zod';
9
+ import { c as createPostSchema, u as updatePostSchema } from './stack.CuhcFsAx.js';
9
10
 
10
11
  /**
11
12
  * Parameters for filtering/paginating posts.
@@ -316,12 +317,6 @@ declare const blogBackendPlugin: (hooks?: BlogBackendHooks) => _btst_stack_plugi
316
317
  title: string;
317
318
  content: string;
318
319
  excerpt: string;
319
- slug?: string | undefined;
320
- published?: boolean | undefined;
321
- publishedAt?: unknown;
322
- createdAt?: unknown;
323
- updatedAt?: unknown;
324
- image?: string | undefined;
325
320
  tags?: ({
326
321
  name: string;
327
322
  } | {
@@ -329,16 +324,16 @@ declare const blogBackendPlugin: (hooks?: BlogBackendHooks) => _btst_stack_plugi
329
324
  name: string;
330
325
  slug: string;
331
326
  })[] | undefined;
332
- }, {
333
- title: string;
334
- content: string;
335
- excerpt: string;
336
327
  slug?: string | undefined;
337
328
  published?: boolean | undefined;
338
329
  publishedAt?: unknown;
339
330
  createdAt?: unknown;
340
331
  updatedAt?: unknown;
341
332
  image?: string | undefined;
333
+ }, {
334
+ title: string;
335
+ content: string;
336
+ excerpt: string;
342
337
  tags?: ({
343
338
  name: string;
344
339
  } | {
@@ -346,6 +341,12 @@ declare const blogBackendPlugin: (hooks?: BlogBackendHooks) => _btst_stack_plugi
346
341
  name: string;
347
342
  slug: string;
348
343
  })[] | undefined;
344
+ slug?: string | undefined;
345
+ published?: boolean | undefined;
346
+ publishedAt?: unknown;
347
+ createdAt?: unknown;
348
+ updatedAt?: unknown;
349
+ image?: string | undefined;
349
350
  }>;
350
351
  }, Post>;
351
352
  readonly updatePost: better_call.StrictEndpoint<"/posts/:id", {} & {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@btst/stack",
3
- "version": "2.11.3",
3
+ "version": "2.11.5",
4
4
  "description": "A composable, plugin-based library for building full-stack applications.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -127,6 +127,16 @@
127
127
  "default": "./dist/plugins/blog/client/index.cjs"
128
128
  }
129
129
  },
130
+ "./plugins/blog/client/components": {
131
+ "import": {
132
+ "types": "./dist/plugins/blog/client/components/index.d.ts",
133
+ "default": "./dist/plugins/blog/client/components/index.mjs"
134
+ },
135
+ "require": {
136
+ "types": "./dist/plugins/blog/client/components/index.d.cts",
137
+ "default": "./dist/plugins/blog/client/components/index.cjs"
138
+ }
139
+ },
130
140
  "./plugins/blog/client/hooks": {
131
141
  "import": {
132
142
  "types": "./dist/plugins/blog/client/hooks/index.d.ts",
@@ -607,6 +617,9 @@
607
617
  "plugins/blog/client": [
608
618
  "./dist/plugins/blog/client/index.d.ts"
609
619
  ],
620
+ "plugins/blog/client/components": [
621
+ "./dist/plugins/blog/client/components/index.d.ts"
622
+ ],
610
623
  "plugins/blog/client/hooks": [
611
624
  "./dist/plugins/blog/client/hooks/index.d.ts"
612
625
  ],
@@ -3,9 +3,61 @@ import { HomePageComponent as PostListPageImpl } from "./pages/home-page";
3
3
  import { NewPostPageComponent as NewPostPageImpl } from "./pages/new-post-page";
4
4
  import { PostPageComponent as PostPageImpl } from "./pages/post-page";
5
5
  import { EditPostPageComponent as EditPostPageImpl } from "./pages/edit-post-page";
6
+ import { PostCard as PostCardImpl } from "./shared/post-card";
7
+ import { PostsList as PostsListImpl } from "./shared/posts-list";
8
+ import { EmptyList as EmptyListImpl } from "./shared/empty-list";
9
+ import { RecentPostsCarousel as RecentPostsCarouselImpl } from "./shared/recent-posts-carousel";
10
+ import { PostNavigation as PostNavigationImpl } from "./shared/post-navigation";
11
+ import { TagsList as TagsListImpl } from "./shared/tags-list";
12
+ import { PostCardSkeleton as PostCardSkeletonImpl } from "./loading/post-card-skeleton";
6
13
 
7
14
  // Re-export to ensure the client boundary is preserved
8
15
  export const PostListPage = PostListPageImpl;
9
16
  export const NewPostPage = NewPostPageImpl;
10
17
  export const PostPage = PostPageImpl;
11
18
  export const EditPostPage = EditPostPageImpl;
19
+
20
+ /**
21
+ * Card component that renders a single blog post summary
22
+ * (cover image, title, date, tags). Used by the built-in posts list and
23
+ * available for composing custom blog landing pages.
24
+ *
25
+ * Requires a `StackProvider` with the blog plugin registered so that
26
+ * `Link` and `Image` overrides are resolved correctly.
27
+ */
28
+ export const PostCard = PostCardImpl;
29
+
30
+ /**
31
+ * Skeleton placeholder that mirrors the layout of {@link PostCard}.
32
+ * Use inside a `<Suspense>` fallback or while data is loading.
33
+ */
34
+ export const PostCardSkeleton = PostCardSkeletonImpl;
35
+
36
+ /**
37
+ * Grid of {@link PostCard}s with optional load-more pagination and a
38
+ * built-in search input. Pass an array of `SerializedPost`s.
39
+ */
40
+ export const PostsList = PostsListImpl;
41
+
42
+ /**
43
+ * Empty-state placeholder used by the built-in blog list pages.
44
+ * Renders a centered icon and a customisable message.
45
+ */
46
+ export const EmptyList = EmptyListImpl;
47
+
48
+ /**
49
+ * Horizontal carousel of recent posts. Drop-in component for "you might
50
+ * also like" sections on a blog post page.
51
+ */
52
+ export const RecentPostsCarousel = RecentPostsCarouselImpl;
53
+
54
+ /**
55
+ * Previous/next post navigation strip, typically rendered at the bottom
56
+ * of a post page.
57
+ */
58
+ export const PostNavigation = PostNavigationImpl;
59
+
60
+ /**
61
+ * Renders a list of tags for a post or for the whole blog.
62
+ */
63
+ export const TagsList = TagsListImpl;
@@ -0,0 +1,177 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { z } from "zod";
3
+ import { buildFieldConfigFromJsonSchema } from "@workspace/ui/components/auto-form/helpers";
4
+
5
+ /**
6
+ * Verifies that buildFieldConfigFromJsonSchema recurses into nested objects AND
7
+ * array items.properties so that per-property metadata (label, placeholder,
8
+ * fieldType, etc.) reaches the AutoForm renderer for fields nested inside
9
+ * arrays-of-objects.
10
+ *
11
+ * The CMS auto-form pipeline relies on this for blend-style schemas like:
12
+ * components: z.array(z.object({
13
+ * name: z.string(),
14
+ * compoundId: z.object({ id: z.string() }).meta({ fieldType: "relation" }),
15
+ * }))
16
+ *
17
+ * Without this recursion, AutoFormArray would render the inner items with no
18
+ * field config — placeholders and custom field components on nested array
19
+ * properties would silently disappear.
20
+ */
21
+
22
+ type ConfigMap = Record<string, Record<string, unknown> | undefined>;
23
+
24
+ function getConfig(map: ConfigMap, key: string): Record<string, unknown> {
25
+ const value = map[key];
26
+ if (!value) {
27
+ throw new Error(`Expected config entry for "${key}" but got undefined`);
28
+ }
29
+ return value;
30
+ }
31
+
32
+ describe("buildFieldConfigFromJsonSchema — nested + array recursion", () => {
33
+ it("propagates per-item placeholders into the array's field config", () => {
34
+ const schema = z.object({
35
+ components: z
36
+ .array(
37
+ z.object({
38
+ name: z.string().meta({ placeholder: "e.g. GHK-Cu" }),
39
+ doseLow: z.coerce.number().meta({ placeholder: "1" }),
40
+ }),
41
+ )
42
+ .default([])
43
+ .meta({ description: "Blend components" }),
44
+ });
45
+
46
+ const jsonSchema = z.toJSONSchema(schema) as Record<string, unknown>;
47
+ const config = buildFieldConfigFromJsonSchema(jsonSchema) as ConfigMap;
48
+ const components = getConfig(config, "components") as ConfigMap & {
49
+ description?: string;
50
+ };
51
+
52
+ expect(components.description).toBe("Blend components");
53
+
54
+ // Per-item configs must live as keys on the array's FieldConfigObject so
55
+ // that AutoFormObject (rendered per array item) can look them up by name.
56
+ const nameConfig = getConfig(components, "name") as {
57
+ inputProps?: { placeholder?: string };
58
+ };
59
+ expect(nameConfig.inputProps?.placeholder).toBe("e.g. GHK-Cu");
60
+
61
+ const doseLowConfig = getConfig(components, "doseLow") as {
62
+ inputProps?: { placeholder?: string };
63
+ };
64
+ expect(doseLowConfig.inputProps?.placeholder).toBe("1");
65
+ });
66
+
67
+ it("propagates fieldType (textarea) into array items", () => {
68
+ const schema = z.object({
69
+ components: z
70
+ .array(
71
+ z.object({
72
+ notes: z.string().meta({ fieldType: "textarea" }),
73
+ }),
74
+ )
75
+ .default([]),
76
+ });
77
+
78
+ const jsonSchema = z.toJSONSchema(schema) as Record<string, unknown>;
79
+ const config = buildFieldConfigFromJsonSchema(jsonSchema) as ConfigMap;
80
+ const components = getConfig(config, "components") as ConfigMap;
81
+ const notesConfig = getConfig(components, "notes") as {
82
+ fieldType?: string;
83
+ };
84
+
85
+ expect(notesConfig.fieldType).toBe("textarea");
86
+ });
87
+
88
+ it("invokes a custom fieldComponent for nested array fields", () => {
89
+ const schema = z.object({
90
+ components: z
91
+ .array(
92
+ z.object({
93
+ compoundId: z
94
+ .object({ id: z.string() })
95
+ .meta({ fieldType: "relation" }),
96
+ }),
97
+ )
98
+ .default([]),
99
+ });
100
+
101
+ const jsonSchema = z.toJSONSchema(schema) as Record<string, unknown>;
102
+
103
+ const RelationStub = () => null;
104
+ const config = buildFieldConfigFromJsonSchema(jsonSchema, {
105
+ relation: RelationStub,
106
+ }) as ConfigMap;
107
+
108
+ const components = getConfig(config, "components") as ConfigMap;
109
+ const compoundIdConfig = getConfig(components, "compoundId") as {
110
+ fieldType?: unknown;
111
+ };
112
+
113
+ // fieldComponents["relation"] should be wired up for the nested array
114
+ // element field, not just top-level fields.
115
+ expect(typeof compoundIdConfig.fieldType).toBe("function");
116
+ });
117
+
118
+ it("still propagates per-property configs into nested objects (regression)", () => {
119
+ const schema = z.object({
120
+ seo: z.object({
121
+ title: z.string().meta({ placeholder: "Page title" }),
122
+ }),
123
+ });
124
+
125
+ const jsonSchema = z.toJSONSchema(schema) as Record<string, unknown>;
126
+ const config = buildFieldConfigFromJsonSchema(jsonSchema) as ConfigMap;
127
+ const seo = getConfig(config, "seo") as ConfigMap;
128
+ const titleConfig = getConfig(seo, "title") as {
129
+ inputProps?: { placeholder?: string };
130
+ };
131
+
132
+ expect(titleConfig.inputProps?.placeholder).toBe("Page title");
133
+ });
134
+
135
+ it("warns and skips item properties whose names collide with reserved FieldConfigItem keys", () => {
136
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
137
+
138
+ const schema = z.object({
139
+ items: z
140
+ .array(
141
+ z.object({
142
+ // "label" is a reserved key on FieldConfigItem and would
143
+ // silently overwrite the array's own label if not guarded.
144
+ label: z.string().meta({ placeholder: "Item label" }),
145
+ value: z.string().meta({ placeholder: "Item value" }),
146
+ }),
147
+ )
148
+ .default([])
149
+ .meta({ description: "An array" }),
150
+ });
151
+
152
+ const jsonSchema = z.toJSONSchema(schema) as Record<string, unknown>;
153
+ const config = buildFieldConfigFromJsonSchema(jsonSchema) as ConfigMap;
154
+ const items = getConfig(config, "items") as ConfigMap & {
155
+ description?: string;
156
+ };
157
+
158
+ // The array's own description should remain intact (not overwritten by
159
+ // a nested item field also named "description").
160
+ expect(items.description).toBe("An array");
161
+
162
+ // Non-reserved item properties propagate normally.
163
+ const valueConfig = getConfig(items, "value") as {
164
+ inputProps?: { placeholder?: string };
165
+ };
166
+ expect(valueConfig.inputProps?.placeholder).toBe("Item value");
167
+
168
+ // The "label" item property collides with a reserved FieldConfigItem
169
+ // key and must be skipped + warned about.
170
+ expect(warn).toHaveBeenCalledWith(
171
+ expect.stringContaining(
172
+ 'Array field "items" has an item property named "label"',
173
+ ),
174
+ );
175
+ warn.mockRestore();
176
+ });
177
+ });
@@ -15,5 +15,6 @@ export {
15
15
  export {
16
16
  createCMSContentItem,
17
17
  type CreateCMSContentItemInput,
18
+ type CreateCMSContentItemOptions,
18
19
  } from "./mutations";
19
20
  export { CMS_QUERY_KEYS } from "./query-key-defs";