@btst/stack 3.1.1 → 3.1.2

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 (26) hide show
  1. package/dist/packages/stack/src/plugins/cms/api/plugin.cjs +12 -0
  2. package/dist/packages/stack/src/plugins/cms/api/plugin.mjs +12 -0
  3. package/dist/plugins/blog/api/index.d.cts +1 -1
  4. package/dist/plugins/blog/api/index.d.mts +1 -1
  5. package/dist/plugins/blog/api/index.d.ts +1 -1
  6. package/dist/plugins/blog/query-keys.d.cts +1 -1
  7. package/dist/plugins/blog/query-keys.d.mts +1 -1
  8. package/dist/plugins/blog/query-keys.d.ts +1 -1
  9. package/dist/plugins/form-builder/api/index.d.cts +1 -1
  10. package/dist/plugins/form-builder/api/index.d.mts +1 -1
  11. package/dist/plugins/form-builder/api/index.d.ts +1 -1
  12. package/dist/plugins/form-builder/client/hooks/index.d.cts +1 -1
  13. package/dist/plugins/form-builder/client/hooks/index.d.mts +1 -1
  14. package/dist/plugins/form-builder/client/hooks/index.d.ts +1 -1
  15. package/dist/plugins/form-builder/query-keys.d.cts +1 -1
  16. package/dist/plugins/form-builder/query-keys.d.mts +1 -1
  17. package/dist/plugins/form-builder/query-keys.d.ts +1 -1
  18. package/package.json +1 -1
  19. package/src/plugins/cms/__tests__/sync-content-types.test.ts +131 -0
  20. package/src/plugins/cms/api/plugin.ts +30 -0
  21. package/dist/shared/{stack.CTneIW0u.d.mts → stack.BggTHKlo.d.mts} +2 -2
  22. package/dist/shared/{stack.BuxUQqL8.d.ts → stack.CGmh-Lpe.d.ts} +12 -12
  23. package/dist/shared/{stack.Ce6V7eDj.d.cts → stack.CtrnoQgH.d.cts} +2 -2
  24. package/dist/shared/{stack.Egr3RWhA.d.mts → stack.Doy2_wgl.d.mts} +12 -12
  25. package/dist/shared/{stack.D5b9l24t.d.ts → stack.lPKwd_v0.d.ts} +2 -2
  26. package/dist/shared/{stack.CX83YNuS.d.cts → stack.zch70Loj.d.cts} +12 -12
@@ -9,6 +9,16 @@ const getters = require('./getters.cjs');
9
9
  const operations = require('./operations.cjs');
10
10
  const queryKeyDefs = require('./query-key-defs.cjs');
11
11
 
12
+ function schemasEqual(stored, configured) {
13
+ const normalize = (key, value) => key !== "properties" && value && typeof value === "object" && !Array.isArray(value) ? Object.fromEntries(
14
+ Object.entries(value).sort(([a], [b]) => a.localeCompare(b))
15
+ ) : value;
16
+ try {
17
+ return JSON.stringify(JSON.parse(stored), normalize) === JSON.stringify(JSON.parse(configured), normalize);
18
+ } catch {
19
+ return false;
20
+ }
21
+ }
12
22
  async function syncContentTypes(adapter, config) {
13
23
  for (const definition of config.contentTypes) {
14
24
  const jsonSchema = JSON.stringify(schemaConverter.zodToFormSchema(definition.schema));
@@ -19,6 +29,8 @@ async function syncContentTypes(adapter, config) {
19
29
  ]
20
30
  });
21
31
  if (existing) {
32
+ if (existing.name === definition.name && (existing.description ?? null) === (definition.description ?? null) && schemasEqual(existing.jsonSchema, jsonSchema) && existing.fieldConfig == null && existing.autoFormVersion === 2)
33
+ continue;
22
34
  await adapter.update({
23
35
  model: "contentType",
24
36
  where: [{ field: "id", value: existing.id, operator: "eq" }],
@@ -7,6 +7,16 @@ import { getContentItemById, getAllContentItems, getAllContentTypes } from './ge
7
7
  import { createCMSOperations, CMSContentTypeParamsSchema, CMSContentItemParamsSchema, CMSCreateContentItemBodySchema, CMSUpdateContentItemBodySchema } from './operations.mjs';
8
8
  import { CMS_QUERY_KEYS } from './query-key-defs.mjs';
9
9
 
10
+ function schemasEqual(stored, configured) {
11
+ const normalize = (key, value) => key !== "properties" && value && typeof value === "object" && !Array.isArray(value) ? Object.fromEntries(
12
+ Object.entries(value).sort(([a], [b]) => a.localeCompare(b))
13
+ ) : value;
14
+ try {
15
+ return JSON.stringify(JSON.parse(stored), normalize) === JSON.stringify(JSON.parse(configured), normalize);
16
+ } catch {
17
+ return false;
18
+ }
19
+ }
10
20
  async function syncContentTypes(adapter, config) {
11
21
  for (const definition of config.contentTypes) {
12
22
  const jsonSchema = JSON.stringify(zodToFormSchema(definition.schema));
@@ -17,6 +27,8 @@ async function syncContentTypes(adapter, config) {
17
27
  ]
18
28
  });
19
29
  if (existing) {
30
+ if (existing.name === definition.name && (existing.description ?? null) === (definition.description ?? null) && schemasEqual(existing.jsonSchema, jsonSchema) && existing.fieldConfig == null && existing.autoFormVersion === 2)
31
+ continue;
20
32
  await adapter.update({
21
33
  model: "contentType",
22
34
  where: [{ field: "id", value: existing.id, operator: "eq" }],
@@ -1,4 +1,4 @@
1
- export { B as BLOG_QUERY_KEYS, i as BlogApiRouter, k as BlogBackendHooks, f as BlogBackendOptions, l as BlogCreateErrorContext, m as BlogCreateOperationContext, n as BlogCreateResultContext, o as BlogDeleteErrorContext, p as BlogDeleteOperationContext, q as BlogDeleteResultContext, r as BlogListErrorContext, s as BlogListOperationContext, t as BlogListResultContext, u as BlogNextPreviousErrorContext, v as BlogNextPreviousOperationContext, w as BlogNextPreviousResultContext, e as BlogRouteKey, x as BlogUpdateErrorContext, y as BlogUpdateOperationContext, z as BlogUpdateResultContext, C as CreatePostOperationInputSchema, N as NextPreviousPostsQuerySchema, P as PostListParams, j as PostListQuerySchema, c as PostListResult, S as SerializedNextPreviousPostsResult, A as SerializedPostListResult, U as UpdatePostOperationInputSchema, h as blogBackendPlugin, d as createBlogQueryKeys, g as getAllPosts, b as getAllTags, a as getPostBySlug } from '../../../shared/stack.Ce6V7eDj.cjs';
1
+ export { B as BLOG_QUERY_KEYS, i as BlogApiRouter, k as BlogBackendHooks, f as BlogBackendOptions, l as BlogCreateErrorContext, m as BlogCreateOperationContext, n as BlogCreateResultContext, o as BlogDeleteErrorContext, p as BlogDeleteOperationContext, q as BlogDeleteResultContext, r as BlogListErrorContext, s as BlogListOperationContext, t as BlogListResultContext, u as BlogNextPreviousErrorContext, v as BlogNextPreviousOperationContext, w as BlogNextPreviousResultContext, e as BlogRouteKey, x as BlogUpdateErrorContext, y as BlogUpdateOperationContext, z as BlogUpdateResultContext, C as CreatePostOperationInputSchema, N as NextPreviousPostsQuerySchema, P as PostListParams, j as PostListQuerySchema, c as PostListResult, S as SerializedNextPreviousPostsResult, A as SerializedPostListResult, U as UpdatePostOperationInputSchema, h as blogBackendPlugin, d as createBlogQueryKeys, g as getAllPosts, b as getAllTags, a as getPostBySlug } from '../../../shared/stack.CtrnoQgH.cjs';
2
2
  import { DBAdapter } from '@btst/db';
3
3
  import { P as Post, T as Tag, S as SerializedPost, a as SerializedTag } from '../../../shared/stack.Dsatjh0B.cjs';
4
4
  export { blogPermissions } from '../permissions.cjs';
@@ -1,4 +1,4 @@
1
- export { B as BLOG_QUERY_KEYS, i as BlogApiRouter, k as BlogBackendHooks, f as BlogBackendOptions, l as BlogCreateErrorContext, m as BlogCreateOperationContext, n as BlogCreateResultContext, o as BlogDeleteErrorContext, p as BlogDeleteOperationContext, q as BlogDeleteResultContext, r as BlogListErrorContext, s as BlogListOperationContext, t as BlogListResultContext, u as BlogNextPreviousErrorContext, v as BlogNextPreviousOperationContext, w as BlogNextPreviousResultContext, e as BlogRouteKey, x as BlogUpdateErrorContext, y as BlogUpdateOperationContext, z as BlogUpdateResultContext, C as CreatePostOperationInputSchema, N as NextPreviousPostsQuerySchema, P as PostListParams, j as PostListQuerySchema, c as PostListResult, S as SerializedNextPreviousPostsResult, A as SerializedPostListResult, U as UpdatePostOperationInputSchema, h as blogBackendPlugin, d as createBlogQueryKeys, g as getAllPosts, b as getAllTags, a as getPostBySlug } from '../../../shared/stack.CTneIW0u.mjs';
1
+ export { B as BLOG_QUERY_KEYS, i as BlogApiRouter, k as BlogBackendHooks, f as BlogBackendOptions, l as BlogCreateErrorContext, m as BlogCreateOperationContext, n as BlogCreateResultContext, o as BlogDeleteErrorContext, p as BlogDeleteOperationContext, q as BlogDeleteResultContext, r as BlogListErrorContext, s as BlogListOperationContext, t as BlogListResultContext, u as BlogNextPreviousErrorContext, v as BlogNextPreviousOperationContext, w as BlogNextPreviousResultContext, e as BlogRouteKey, x as BlogUpdateErrorContext, y as BlogUpdateOperationContext, z as BlogUpdateResultContext, C as CreatePostOperationInputSchema, N as NextPreviousPostsQuerySchema, P as PostListParams, j as PostListQuerySchema, c as PostListResult, S as SerializedNextPreviousPostsResult, A as SerializedPostListResult, U as UpdatePostOperationInputSchema, h as blogBackendPlugin, d as createBlogQueryKeys, g as getAllPosts, b as getAllTags, a as getPostBySlug } from '../../../shared/stack.BggTHKlo.mjs';
2
2
  import { DBAdapter } from '@btst/db';
3
3
  import { P as Post, T as Tag, S as SerializedPost, a as SerializedTag } from '../../../shared/stack.Dsatjh0B.mjs';
4
4
  export { blogPermissions } from '../permissions.mjs';
@@ -1,4 +1,4 @@
1
- export { B as BLOG_QUERY_KEYS, i as BlogApiRouter, k as BlogBackendHooks, f as BlogBackendOptions, l as BlogCreateErrorContext, m as BlogCreateOperationContext, n as BlogCreateResultContext, o as BlogDeleteErrorContext, p as BlogDeleteOperationContext, q as BlogDeleteResultContext, r as BlogListErrorContext, s as BlogListOperationContext, t as BlogListResultContext, u as BlogNextPreviousErrorContext, v as BlogNextPreviousOperationContext, w as BlogNextPreviousResultContext, e as BlogRouteKey, x as BlogUpdateErrorContext, y as BlogUpdateOperationContext, z as BlogUpdateResultContext, C as CreatePostOperationInputSchema, N as NextPreviousPostsQuerySchema, P as PostListParams, j as PostListQuerySchema, c as PostListResult, S as SerializedNextPreviousPostsResult, A as SerializedPostListResult, U as UpdatePostOperationInputSchema, h as blogBackendPlugin, d as createBlogQueryKeys, g as getAllPosts, b as getAllTags, a as getPostBySlug } from '../../../shared/stack.D5b9l24t.js';
1
+ export { B as BLOG_QUERY_KEYS, i as BlogApiRouter, k as BlogBackendHooks, f as BlogBackendOptions, l as BlogCreateErrorContext, m as BlogCreateOperationContext, n as BlogCreateResultContext, o as BlogDeleteErrorContext, p as BlogDeleteOperationContext, q as BlogDeleteResultContext, r as BlogListErrorContext, s as BlogListOperationContext, t as BlogListResultContext, u as BlogNextPreviousErrorContext, v as BlogNextPreviousOperationContext, w as BlogNextPreviousResultContext, e as BlogRouteKey, x as BlogUpdateErrorContext, y as BlogUpdateOperationContext, z as BlogUpdateResultContext, C as CreatePostOperationInputSchema, N as NextPreviousPostsQuerySchema, P as PostListParams, j as PostListQuerySchema, c as PostListResult, S as SerializedNextPreviousPostsResult, A as SerializedPostListResult, U as UpdatePostOperationInputSchema, h as blogBackendPlugin, d as createBlogQueryKeys, g as getAllPosts, b as getAllTags, a as getPostBySlug } from '../../../shared/stack.lPKwd_v0.js';
2
2
  import { DBAdapter } from '@btst/db';
3
3
  import { P as Post, T as Tag, S as SerializedPost, a as SerializedTag } from '../../../shared/stack.Dsatjh0B.js';
4
4
  export { blogPermissions } from '../permissions.js';
@@ -1,5 +1,5 @@
1
1
  import '@btst/stack/plugins/client';
2
- export { D as blogResources, d as createBlogQueryKeys } from '../../shared/stack.Ce6V7eDj.cjs';
2
+ export { D as blogResources, d as createBlogQueryKeys } from '../../shared/stack.CtrnoQgH.cjs';
3
3
  import 'zod';
4
4
  import '../../shared/stack.CKc_Bais.cjs';
5
5
  import '../../shared/stack.Dsatjh0B.cjs';
@@ -1,5 +1,5 @@
1
1
  import '@btst/stack/plugins/client';
2
- export { D as blogResources, d as createBlogQueryKeys } from '../../shared/stack.CTneIW0u.mjs';
2
+ export { D as blogResources, d as createBlogQueryKeys } from '../../shared/stack.BggTHKlo.mjs';
3
3
  import 'zod';
4
4
  import '../../shared/stack.CKc_Bais.mjs';
5
5
  import '../../shared/stack.Dsatjh0B.mjs';
@@ -1,5 +1,5 @@
1
1
  import '@btst/stack/plugins/client';
2
- export { D as blogResources, d as createBlogQueryKeys } from '../../shared/stack.D5b9l24t.js';
2
+ export { D as blogResources, d as createBlogQueryKeys } from '../../shared/stack.lPKwd_v0.js';
3
3
  import 'zod';
4
4
  import '../../shared/stack.CKc_Bais.js';
5
5
  import '../../shared/stack.Dsatjh0B.js';
@@ -1,4 +1,4 @@
1
- export { k as FORM_QUERY_KEYS, a as FormBuilderApiRouter, c as FormBuilderOperationError, e as FormBuilderOperations, b as FormBuilderRouteKey, g as FormListOperationResult, h as SubmissionDetailOperationResult, i as SubmissionListOperationResult, j as SubmitOperationResult, d as createFormBuilderOperations, f as formBuilderBackendPlugin } from '../../../shared/stack.CX83YNuS.cjs';
1
+ export { k as FORM_QUERY_KEYS, a as FormBuilderApiRouter, c as FormBuilderOperationError, e as FormBuilderOperations, b as FormBuilderRouteKey, g as FormListOperationResult, h as SubmissionDetailOperationResult, i as SubmissionListOperationResult, j as SubmitOperationResult, d as createFormBuilderOperations, f as formBuilderBackendPlugin } from '../../../shared/stack.zch70Loj.cjs';
2
2
  export { formBuilderPermissions } from '../permissions.cjs';
3
3
  import { S as SerializedForm, d as SerializedFormSubmissionWithData, F as Form, f as FormSubmission, a as SerializedFormSubmission, c as SerializedFormSubmissionSummary, g as FormSubmissionWithForm } from '../../../shared/stack.v2O65JqF.cjs';
4
4
  export { h as FormBuilderBackendConfig, i as FormBuilderBackendHooks, j as FormBuilderHookContext, k as FormBuilderOperationHookContext, l as FormCreateOperationContext, m as FormDeleteOperationContext, o as FormGetForUpdateOperationContext, n as FormGetOperationContext, p as FormListOperationContext, q as FormUpdateOperationContext, r as SubmissionCreateOperationContext, s as SubmissionDeleteOperationContext, t as SubmissionGetOperationContext, v as SubmissionHookContext, b as SubmissionListFormContext, u as SubmissionListOperationContext } from '../../../shared/stack.v2O65JqF.cjs';
@@ -1,4 +1,4 @@
1
- export { k as FORM_QUERY_KEYS, a as FormBuilderApiRouter, c as FormBuilderOperationError, e as FormBuilderOperations, b as FormBuilderRouteKey, g as FormListOperationResult, h as SubmissionDetailOperationResult, i as SubmissionListOperationResult, j as SubmitOperationResult, d as createFormBuilderOperations, f as formBuilderBackendPlugin } from '../../../shared/stack.Egr3RWhA.mjs';
1
+ export { k as FORM_QUERY_KEYS, a as FormBuilderApiRouter, c as FormBuilderOperationError, e as FormBuilderOperations, b as FormBuilderRouteKey, g as FormListOperationResult, h as SubmissionDetailOperationResult, i as SubmissionListOperationResult, j as SubmitOperationResult, d as createFormBuilderOperations, f as formBuilderBackendPlugin } from '../../../shared/stack.Doy2_wgl.mjs';
2
2
  export { formBuilderPermissions } from '../permissions.mjs';
3
3
  import { S as SerializedForm, d as SerializedFormSubmissionWithData, F as Form, f as FormSubmission, a as SerializedFormSubmission, c as SerializedFormSubmissionSummary, g as FormSubmissionWithForm } from '../../../shared/stack.OYT8WU4n.mjs';
4
4
  export { h as FormBuilderBackendConfig, i as FormBuilderBackendHooks, j as FormBuilderHookContext, k as FormBuilderOperationHookContext, l as FormCreateOperationContext, m as FormDeleteOperationContext, o as FormGetForUpdateOperationContext, n as FormGetOperationContext, p as FormListOperationContext, q as FormUpdateOperationContext, r as SubmissionCreateOperationContext, s as SubmissionDeleteOperationContext, t as SubmissionGetOperationContext, v as SubmissionHookContext, b as SubmissionListFormContext, u as SubmissionListOperationContext } from '../../../shared/stack.OYT8WU4n.mjs';
@@ -1,4 +1,4 @@
1
- export { k as FORM_QUERY_KEYS, a as FormBuilderApiRouter, c as FormBuilderOperationError, e as FormBuilderOperations, b as FormBuilderRouteKey, g as FormListOperationResult, h as SubmissionDetailOperationResult, i as SubmissionListOperationResult, j as SubmitOperationResult, d as createFormBuilderOperations, f as formBuilderBackendPlugin } from '../../../shared/stack.BuxUQqL8.js';
1
+ export { k as FORM_QUERY_KEYS, a as FormBuilderApiRouter, c as FormBuilderOperationError, e as FormBuilderOperations, b as FormBuilderRouteKey, g as FormListOperationResult, h as SubmissionDetailOperationResult, i as SubmissionListOperationResult, j as SubmitOperationResult, d as createFormBuilderOperations, f as formBuilderBackendPlugin } from '../../../shared/stack.CGmh-Lpe.js';
2
2
  export { formBuilderPermissions } from '../permissions.js';
3
3
  import { S as SerializedForm, d as SerializedFormSubmissionWithData, F as Form, f as FormSubmission, a as SerializedFormSubmission, c as SerializedFormSubmissionSummary, g as FormSubmissionWithForm } from '../../../shared/stack.C3bU9w6L.js';
4
4
  export { h as FormBuilderBackendConfig, i as FormBuilderBackendHooks, j as FormBuilderHookContext, k as FormBuilderOperationHookContext, l as FormCreateOperationContext, m as FormDeleteOperationContext, o as FormGetForUpdateOperationContext, n as FormGetOperationContext, p as FormListOperationContext, q as FormUpdateOperationContext, r as SubmissionCreateOperationContext, s as SubmissionDeleteOperationContext, t as SubmissionGetOperationContext, v as SubmissionHookContext, b as SubmissionListFormContext, u as SubmissionListOperationContext } from '../../../shared/stack.C3bU9w6L.js';
@@ -8,7 +8,7 @@ import '@btst/stack/context';
8
8
  import '../../permissions.cjs';
9
9
  import 'zod';
10
10
  import '@btst/stack/plugins/client';
11
- import '../../../../shared/stack.CX83YNuS.cjs';
11
+ import '../../../../shared/stack.zch70Loj.cjs';
12
12
  import '@btst/db';
13
13
  import 'zod/v4/core';
14
14
  import 'better-call';
@@ -8,7 +8,7 @@ import '@btst/stack/context';
8
8
  import '../../permissions.mjs';
9
9
  import 'zod';
10
10
  import '@btst/stack/plugins/client';
11
- import '../../../../shared/stack.Egr3RWhA.mjs';
11
+ import '../../../../shared/stack.Doy2_wgl.mjs';
12
12
  import '@btst/db';
13
13
  import 'zod/v4/core';
14
14
  import 'better-call';
@@ -8,7 +8,7 @@ import '@btst/stack/context';
8
8
  import '../../permissions.js';
9
9
  import 'zod';
10
10
  import '@btst/stack/plugins/client';
11
- import '../../../../shared/stack.BuxUQqL8.js';
11
+ import '../../../../shared/stack.CGmh-Lpe.js';
12
12
  import '@btst/db';
13
13
  import 'zod/v4/core';
14
14
  import 'better-call';
@@ -1,6 +1,6 @@
1
1
  import * as _btst_stack_plugins_client from '@btst/stack/plugins/client';
2
2
  import { createApiClient } from '@btst/stack/plugins/client';
3
- import { F as FormsListDiscriminator, S as SubmissionsListDiscriminator, a as FormBuilderApiRouter } from '../../shared/stack.CX83YNuS.cjs';
3
+ import { F as FormsListDiscriminator, S as SubmissionsListDiscriminator, a as FormBuilderApiRouter } from '../../shared/stack.zch70Loj.cjs';
4
4
  import { P as PaginatedForms, S as SerializedForm, a as SerializedFormSubmission, e as PaginatedFormSubmissions, d as SerializedFormSubmissionWithData } from '../../shared/stack.v2O65JqF.cjs';
5
5
  import { StackIdentity } from '@btst/stack/context';
6
6
  import '@btst/stack/plugins/api';
@@ -1,6 +1,6 @@
1
1
  import * as _btst_stack_plugins_client from '@btst/stack/plugins/client';
2
2
  import { createApiClient } from '@btst/stack/plugins/client';
3
- import { F as FormsListDiscriminator, S as SubmissionsListDiscriminator, a as FormBuilderApiRouter } from '../../shared/stack.Egr3RWhA.mjs';
3
+ import { F as FormsListDiscriminator, S as SubmissionsListDiscriminator, a as FormBuilderApiRouter } from '../../shared/stack.Doy2_wgl.mjs';
4
4
  import { P as PaginatedForms, S as SerializedForm, a as SerializedFormSubmission, e as PaginatedFormSubmissions, d as SerializedFormSubmissionWithData } from '../../shared/stack.OYT8WU4n.mjs';
5
5
  import { StackIdentity } from '@btst/stack/context';
6
6
  import '@btst/stack/plugins/api';
@@ -1,6 +1,6 @@
1
1
  import * as _btst_stack_plugins_client from '@btst/stack/plugins/client';
2
2
  import { createApiClient } from '@btst/stack/plugins/client';
3
- import { F as FormsListDiscriminator, S as SubmissionsListDiscriminator, a as FormBuilderApiRouter } from '../../shared/stack.BuxUQqL8.js';
3
+ import { F as FormsListDiscriminator, S as SubmissionsListDiscriminator, a as FormBuilderApiRouter } from '../../shared/stack.CGmh-Lpe.js';
4
4
  import { P as PaginatedForms, S as SerializedForm, a as SerializedFormSubmission, e as PaginatedFormSubmissions, d as SerializedFormSubmissionWithData } from '../../shared/stack.C3bU9w6L.js';
5
5
  import { StackIdentity } from '@btst/stack/context';
6
6
  import '@btst/stack/plugins/api';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@btst/stack",
3
- "version": "3.1.1",
3
+ "version": "3.1.2",
4
4
  "description": "A composable, plugin-based library for building full-stack applications.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,131 @@
1
+ import { createMemoryAdapter } from "@btst/adapter-memory";
2
+ import type { DBAdapter } from "@btst/db";
3
+ import { describe, expect, it, vi } from "vitest";
4
+ import { z } from "zod";
5
+ import { createBackendStack } from "../../../api";
6
+ import { cmsBackendPlugin } from "../api";
7
+ import type { ContentType } from "../types";
8
+
9
+ const contentTypes = [
10
+ {
11
+ name: "Article",
12
+ slug: "article",
13
+ schema: z.object({
14
+ title: z.string(),
15
+ status: z.enum(["draft", "published"]),
16
+ }),
17
+ },
18
+ ];
19
+ function coldStart(adapter?: DBAdapter) {
20
+ return createBackendStack({
21
+ basePath: "/api",
22
+ plugins: { cms: cmsBackendPlugin({ contentTypes }) },
23
+ adapter: (schema) => adapter ?? createMemoryAdapter(schema)({}),
24
+ });
25
+ }
26
+ async function fixture() {
27
+ const stack = coldStart();
28
+ await stack.trusted.cms.listContentTypes({});
29
+ const stored = await stack.adapter.findOne<ContentType>({
30
+ model: "contentType",
31
+ where: [{ field: "slug", value: "article" }],
32
+ });
33
+ if (!stored) throw new Error("Missing seeded content type");
34
+ return { adapter: stack.adapter, stored };
35
+ }
36
+
37
+ describe("CMS content-type synchronization", () => {
38
+ it("performs no updates on repeated cold starts and preserves serialized data", async () => {
39
+ const { adapter } = await fixture();
40
+ const update = vi.spyOn(adapter, "update");
41
+ const first = await coldStart(adapter).trusted.cms.listContentTypes({});
42
+ const second = await coldStart(adapter).trusted.cms.listContentTypes({});
43
+ expect(update).not.toHaveBeenCalled();
44
+ expect(JSON.stringify(second)).toBe(JSON.stringify(first));
45
+ });
46
+
47
+ it("ignores JSON whitespace and nested object key order", async () => {
48
+ const { adapter, stored } = await fixture();
49
+ const reordered = JSON.stringify(
50
+ JSON.parse(stored.jsonSchema),
51
+ (key, value) =>
52
+ key !== "properties" &&
53
+ value &&
54
+ typeof value === "object" &&
55
+ !Array.isArray(value)
56
+ ? Object.fromEntries(Object.entries(value).reverse())
57
+ : value,
58
+ 2,
59
+ );
60
+ await adapter.update({
61
+ model: "contentType",
62
+ where: [{ field: "id", value: stored.id }],
63
+ update: { jsonSchema: reordered },
64
+ });
65
+ const update = vi.spyOn(adapter, "update");
66
+ await coldStart(adapter).trusted.cms.listContentTypes({});
67
+ expect(update).not.toHaveBeenCalled();
68
+ });
69
+
70
+ it.each([
71
+ ["name", "Old article"],
72
+ ["description", "Old description"],
73
+ ["jsonSchema", '{"type":"object","properties":{}}'],
74
+ ["jsonSchema", "invalid legacy JSON"],
75
+ ["fieldConfig", '{"title":{"fieldType":"textarea"}}'],
76
+ ["autoFormVersion", 1],
77
+ ] as const)("updates changed %s exactly once", async (field, value) => {
78
+ const { adapter, stored } = await fixture();
79
+ await adapter.update({
80
+ model: "contentType",
81
+ where: [{ field: "id", value: stored.id }],
82
+ update: { [field]: value, updatedAt: new Date("2000-01-01") },
83
+ });
84
+ const update = vi.spyOn(adapter, "update");
85
+ await coldStart(adapter).trusted.cms.listContentTypes({});
86
+ expect(update).toHaveBeenCalledTimes(1);
87
+ expect(update.mock.calls[0]?.[0].update).toMatchObject({
88
+ name: "Article",
89
+ description: null,
90
+ jsonSchema: stored.jsonSchema,
91
+ fieldConfig: null,
92
+ autoFormVersion: 2,
93
+ });
94
+ expect(update.mock.calls[0]?.[0].update.updatedAt).not.toEqual(
95
+ new Date("2000-01-01"),
96
+ );
97
+ await coldStart(adapter).trusted.cms.listContentTypes({});
98
+ expect(update).toHaveBeenCalledTimes(1);
99
+ });
100
+
101
+ it("updates reordered schema properties because they control form field order", async () => {
102
+ const { adapter, stored } = await fixture();
103
+ const schema = JSON.parse(stored.jsonSchema);
104
+ schema.properties = Object.fromEntries(
105
+ Object.entries(schema.properties).reverse(),
106
+ );
107
+ await adapter.update({
108
+ model: "contentType",
109
+ where: [{ field: "id", value: stored.id }],
110
+ update: { jsonSchema: JSON.stringify(schema) },
111
+ });
112
+ const update = vi.spyOn(adapter, "update");
113
+ await coldStart(adapter).trusted.cms.listContentTypes({});
114
+ expect(update).toHaveBeenCalledTimes(1);
115
+ expect(update.mock.calls[0]?.[0].update.jsonSchema).toBe(stored.jsonSchema);
116
+ });
117
+
118
+ it("preserves meaningful array ordering in schema comparisons", async () => {
119
+ const { adapter, stored } = await fixture();
120
+ const schema = JSON.parse(stored.jsonSchema);
121
+ schema.properties.status.enum.reverse();
122
+ await adapter.update({
123
+ model: "contentType",
124
+ where: [{ field: "id", value: stored.id }],
125
+ update: { jsonSchema: JSON.stringify(schema) },
126
+ });
127
+ const update = vi.spyOn(adapter, "update");
128
+ await coldStart(adapter).trusted.cms.listContentTypes({});
129
+ expect(update).toHaveBeenCalledTimes(1);
130
+ });
131
+ });
@@ -63,6 +63,28 @@ interface CMSPrefetchForRoute {
63
63
  ): Promise<void>;
64
64
  }
65
65
 
66
+ /** Preserve form field order while normalizing JSON metadata key order. */
67
+ function schemasEqual(stored: string, configured: string): boolean {
68
+ const normalize = (key: string, value: unknown): unknown =>
69
+ key !== "properties" &&
70
+ value &&
71
+ typeof value === "object" &&
72
+ !Array.isArray(value)
73
+ ? Object.fromEntries(
74
+ Object.entries(value).sort(([a], [b]) => a.localeCompare(b)),
75
+ )
76
+ : value;
77
+ try {
78
+ return (
79
+ JSON.stringify(JSON.parse(stored), normalize) ===
80
+ JSON.stringify(JSON.parse(configured), normalize)
81
+ );
82
+ } catch {
83
+ // A malformed stored schema still needs repair from the code definition.
84
+ return false;
85
+ }
86
+ }
87
+
66
88
  async function syncContentTypes(
67
89
  adapter: Adapter,
68
90
  config: CMSBackendConfig,
@@ -76,6 +98,14 @@ async function syncContentTypes(
76
98
  ],
77
99
  });
78
100
  if (existing) {
101
+ if (
102
+ existing.name === definition.name &&
103
+ (existing.description ?? null) === (definition.description ?? null) &&
104
+ schemasEqual(existing.jsonSchema, jsonSchema) &&
105
+ existing.fieldConfig == null &&
106
+ existing.autoFormVersion === 2
107
+ )
108
+ continue;
79
109
  await adapter.update({
80
110
  model: "contentType",
81
111
  where: [{ field: "id", value: existing.id, operator: "eq" as const }],
@@ -900,8 +900,8 @@ declare const blogResources: {
900
900
  method: "POST";
901
901
  input: (vars: PostCreateInput) => {
902
902
  body: {
903
- title: string;
904
903
  published: boolean;
904
+ title: string;
905
905
  content: string;
906
906
  excerpt: string;
907
907
  tags: ({
@@ -1072,8 +1072,8 @@ declare function createBlogQueryKeys(client: ReturnType<typeof createApiClient<B
1072
1072
  method: "POST";
1073
1073
  input: (vars: PostCreateInput) => {
1074
1074
  body: {
1075
- title: string;
1076
1075
  published: boolean;
1076
+ title: string;
1077
1077
  content: string;
1078
1078
  excerpt: string;
1079
1079
  tags: ({
@@ -127,11 +127,11 @@ declare const formBuilderBackendPlugin: (config?: FormBuilderBackendConfig) => _
127
127
  readonly status: Form["status"];
128
128
  readonly createdAt: string;
129
129
  readonly updatedAt: string;
130
- readonly schema: string;
131
130
  readonly id: string;
131
+ readonly name: string;
132
+ readonly schema: string;
132
133
  readonly description?: string | undefined;
133
134
  readonly slug: string;
134
- readonly name: string;
135
135
  readonly successMessage?: string | undefined;
136
136
  readonly redirectUrl?: string | undefined;
137
137
  readonly createdBy?: string | undefined;
@@ -151,11 +151,11 @@ declare const formBuilderBackendPlugin: (config?: FormBuilderBackendConfig) => _
151
151
  readonly status: Form["status"];
152
152
  readonly createdAt: string;
153
153
  readonly updatedAt: string;
154
- readonly schema: string;
155
154
  readonly id: string;
155
+ readonly name: string;
156
+ readonly schema: string;
156
157
  readonly description?: string | undefined;
157
158
  readonly slug: string;
158
- readonly name: string;
159
159
  readonly successMessage?: string | undefined;
160
160
  readonly redirectUrl?: string | undefined;
161
161
  readonly createdBy?: string | undefined;
@@ -171,11 +171,11 @@ declare const formBuilderBackendPlugin: (config?: FormBuilderBackendConfig) => _
171
171
  readonly status: Form["status"];
172
172
  readonly createdAt: string;
173
173
  readonly updatedAt: string;
174
- readonly schema: string;
175
174
  readonly id: string;
175
+ readonly name: string;
176
+ readonly schema: string;
176
177
  readonly description?: string | undefined;
177
178
  readonly slug: string;
178
- readonly name: string;
179
179
  readonly successMessage?: string | undefined;
180
180
  readonly redirectUrl?: string | undefined;
181
181
  readonly createdBy?: string | undefined;
@@ -191,11 +191,11 @@ declare const formBuilderBackendPlugin: (config?: FormBuilderBackendConfig) => _
191
191
  readonly status: Form["status"];
192
192
  readonly createdAt: string;
193
193
  readonly updatedAt: string;
194
- readonly schema: string;
195
194
  readonly id: string;
195
+ readonly name: string;
196
+ readonly schema: string;
196
197
  readonly description?: string | undefined;
197
198
  readonly slug: string;
198
- readonly name: string;
199
199
  readonly successMessage?: string | undefined;
200
200
  readonly redirectUrl?: string | undefined;
201
201
  readonly createdBy?: string | undefined;
@@ -225,11 +225,11 @@ declare const formBuilderBackendPlugin: (config?: FormBuilderBackendConfig) => _
225
225
  readonly status: Form["status"];
226
226
  readonly createdAt: string;
227
227
  readonly updatedAt: string;
228
- readonly schema: string;
229
228
  readonly id: string;
229
+ readonly name: string;
230
+ readonly schema: string;
230
231
  readonly description?: string | undefined;
231
232
  readonly slug: string;
232
- readonly name: string;
233
233
  readonly successMessage?: string | undefined;
234
234
  readonly redirectUrl?: string | undefined;
235
235
  readonly createdBy?: string | undefined;
@@ -259,11 +259,11 @@ declare const formBuilderBackendPlugin: (config?: FormBuilderBackendConfig) => _
259
259
  readonly status: Form["status"];
260
260
  readonly createdAt: string;
261
261
  readonly updatedAt: string;
262
- readonly schema: string;
263
262
  readonly id: string;
263
+ readonly name: string;
264
+ readonly schema: string;
264
265
  readonly description?: string | undefined;
265
266
  readonly slug: string;
266
- readonly name: string;
267
267
  readonly successMessage?: string | undefined;
268
268
  readonly redirectUrl?: string | undefined;
269
269
  readonly createdBy?: string | undefined;
@@ -900,8 +900,8 @@ declare const blogResources: {
900
900
  method: "POST";
901
901
  input: (vars: PostCreateInput) => {
902
902
  body: {
903
- title: string;
904
903
  published: boolean;
904
+ title: string;
905
905
  content: string;
906
906
  excerpt: string;
907
907
  tags: ({
@@ -1072,8 +1072,8 @@ declare function createBlogQueryKeys(client: ReturnType<typeof createApiClient<B
1072
1072
  method: "POST";
1073
1073
  input: (vars: PostCreateInput) => {
1074
1074
  body: {
1075
- title: string;
1076
1075
  published: boolean;
1076
+ title: string;
1077
1077
  content: string;
1078
1078
  excerpt: string;
1079
1079
  tags: ({
@@ -127,11 +127,11 @@ declare const formBuilderBackendPlugin: (config?: FormBuilderBackendConfig) => _
127
127
  readonly status: Form["status"];
128
128
  readonly createdAt: string;
129
129
  readonly updatedAt: string;
130
- readonly schema: string;
131
130
  readonly id: string;
131
+ readonly name: string;
132
+ readonly schema: string;
132
133
  readonly description?: string | undefined;
133
134
  readonly slug: string;
134
- readonly name: string;
135
135
  readonly successMessage?: string | undefined;
136
136
  readonly redirectUrl?: string | undefined;
137
137
  readonly createdBy?: string | undefined;
@@ -151,11 +151,11 @@ declare const formBuilderBackendPlugin: (config?: FormBuilderBackendConfig) => _
151
151
  readonly status: Form["status"];
152
152
  readonly createdAt: string;
153
153
  readonly updatedAt: string;
154
- readonly schema: string;
155
154
  readonly id: string;
155
+ readonly name: string;
156
+ readonly schema: string;
156
157
  readonly description?: string | undefined;
157
158
  readonly slug: string;
158
- readonly name: string;
159
159
  readonly successMessage?: string | undefined;
160
160
  readonly redirectUrl?: string | undefined;
161
161
  readonly createdBy?: string | undefined;
@@ -171,11 +171,11 @@ declare const formBuilderBackendPlugin: (config?: FormBuilderBackendConfig) => _
171
171
  readonly status: Form["status"];
172
172
  readonly createdAt: string;
173
173
  readonly updatedAt: string;
174
- readonly schema: string;
175
174
  readonly id: string;
175
+ readonly name: string;
176
+ readonly schema: string;
176
177
  readonly description?: string | undefined;
177
178
  readonly slug: string;
178
- readonly name: string;
179
179
  readonly successMessage?: string | undefined;
180
180
  readonly redirectUrl?: string | undefined;
181
181
  readonly createdBy?: string | undefined;
@@ -191,11 +191,11 @@ declare const formBuilderBackendPlugin: (config?: FormBuilderBackendConfig) => _
191
191
  readonly status: Form["status"];
192
192
  readonly createdAt: string;
193
193
  readonly updatedAt: string;
194
- readonly schema: string;
195
194
  readonly id: string;
195
+ readonly name: string;
196
+ readonly schema: string;
196
197
  readonly description?: string | undefined;
197
198
  readonly slug: string;
198
- readonly name: string;
199
199
  readonly successMessage?: string | undefined;
200
200
  readonly redirectUrl?: string | undefined;
201
201
  readonly createdBy?: string | undefined;
@@ -225,11 +225,11 @@ declare const formBuilderBackendPlugin: (config?: FormBuilderBackendConfig) => _
225
225
  readonly status: Form["status"];
226
226
  readonly createdAt: string;
227
227
  readonly updatedAt: string;
228
- readonly schema: string;
229
228
  readonly id: string;
229
+ readonly name: string;
230
+ readonly schema: string;
230
231
  readonly description?: string | undefined;
231
232
  readonly slug: string;
232
- readonly name: string;
233
233
  readonly successMessage?: string | undefined;
234
234
  readonly redirectUrl?: string | undefined;
235
235
  readonly createdBy?: string | undefined;
@@ -259,11 +259,11 @@ declare const formBuilderBackendPlugin: (config?: FormBuilderBackendConfig) => _
259
259
  readonly status: Form["status"];
260
260
  readonly createdAt: string;
261
261
  readonly updatedAt: string;
262
- readonly schema: string;
263
262
  readonly id: string;
263
+ readonly name: string;
264
+ readonly schema: string;
264
265
  readonly description?: string | undefined;
265
266
  readonly slug: string;
266
- readonly name: string;
267
267
  readonly successMessage?: string | undefined;
268
268
  readonly redirectUrl?: string | undefined;
269
269
  readonly createdBy?: string | undefined;
@@ -900,8 +900,8 @@ declare const blogResources: {
900
900
  method: "POST";
901
901
  input: (vars: PostCreateInput) => {
902
902
  body: {
903
- title: string;
904
903
  published: boolean;
904
+ title: string;
905
905
  content: string;
906
906
  excerpt: string;
907
907
  tags: ({
@@ -1072,8 +1072,8 @@ declare function createBlogQueryKeys(client: ReturnType<typeof createApiClient<B
1072
1072
  method: "POST";
1073
1073
  input: (vars: PostCreateInput) => {
1074
1074
  body: {
1075
- title: string;
1076
1075
  published: boolean;
1076
+ title: string;
1077
1077
  content: string;
1078
1078
  excerpt: string;
1079
1079
  tags: ({
@@ -127,11 +127,11 @@ declare const formBuilderBackendPlugin: (config?: FormBuilderBackendConfig) => _
127
127
  readonly status: Form["status"];
128
128
  readonly createdAt: string;
129
129
  readonly updatedAt: string;
130
- readonly schema: string;
131
130
  readonly id: string;
131
+ readonly name: string;
132
+ readonly schema: string;
132
133
  readonly description?: string | undefined;
133
134
  readonly slug: string;
134
- readonly name: string;
135
135
  readonly successMessage?: string | undefined;
136
136
  readonly redirectUrl?: string | undefined;
137
137
  readonly createdBy?: string | undefined;
@@ -151,11 +151,11 @@ declare const formBuilderBackendPlugin: (config?: FormBuilderBackendConfig) => _
151
151
  readonly status: Form["status"];
152
152
  readonly createdAt: string;
153
153
  readonly updatedAt: string;
154
- readonly schema: string;
155
154
  readonly id: string;
155
+ readonly name: string;
156
+ readonly schema: string;
156
157
  readonly description?: string | undefined;
157
158
  readonly slug: string;
158
- readonly name: string;
159
159
  readonly successMessage?: string | undefined;
160
160
  readonly redirectUrl?: string | undefined;
161
161
  readonly createdBy?: string | undefined;
@@ -171,11 +171,11 @@ declare const formBuilderBackendPlugin: (config?: FormBuilderBackendConfig) => _
171
171
  readonly status: Form["status"];
172
172
  readonly createdAt: string;
173
173
  readonly updatedAt: string;
174
- readonly schema: string;
175
174
  readonly id: string;
175
+ readonly name: string;
176
+ readonly schema: string;
176
177
  readonly description?: string | undefined;
177
178
  readonly slug: string;
178
- readonly name: string;
179
179
  readonly successMessage?: string | undefined;
180
180
  readonly redirectUrl?: string | undefined;
181
181
  readonly createdBy?: string | undefined;
@@ -191,11 +191,11 @@ declare const formBuilderBackendPlugin: (config?: FormBuilderBackendConfig) => _
191
191
  readonly status: Form["status"];
192
192
  readonly createdAt: string;
193
193
  readonly updatedAt: string;
194
- readonly schema: string;
195
194
  readonly id: string;
195
+ readonly name: string;
196
+ readonly schema: string;
196
197
  readonly description?: string | undefined;
197
198
  readonly slug: string;
198
- readonly name: string;
199
199
  readonly successMessage?: string | undefined;
200
200
  readonly redirectUrl?: string | undefined;
201
201
  readonly createdBy?: string | undefined;
@@ -225,11 +225,11 @@ declare const formBuilderBackendPlugin: (config?: FormBuilderBackendConfig) => _
225
225
  readonly status: Form["status"];
226
226
  readonly createdAt: string;
227
227
  readonly updatedAt: string;
228
- readonly schema: string;
229
228
  readonly id: string;
229
+ readonly name: string;
230
+ readonly schema: string;
230
231
  readonly description?: string | undefined;
231
232
  readonly slug: string;
232
- readonly name: string;
233
233
  readonly successMessage?: string | undefined;
234
234
  readonly redirectUrl?: string | undefined;
235
235
  readonly createdBy?: string | undefined;
@@ -259,11 +259,11 @@ declare const formBuilderBackendPlugin: (config?: FormBuilderBackendConfig) => _
259
259
  readonly status: Form["status"];
260
260
  readonly createdAt: string;
261
261
  readonly updatedAt: string;
262
- readonly schema: string;
263
262
  readonly id: string;
263
+ readonly name: string;
264
+ readonly schema: string;
264
265
  readonly description?: string | undefined;
265
266
  readonly slug: string;
266
- readonly name: string;
267
267
  readonly successMessage?: string | undefined;
268
268
  readonly redirectUrl?: string | undefined;
269
269
  readonly createdBy?: string | undefined;