@btst/stack 2.11.2 → 2.11.4

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 (41) 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/plugins/blog/api/index.d.cts +2 -2
  8. package/dist/plugins/blog/api/index.d.mts +2 -2
  9. package/dist/plugins/blog/api/index.d.ts +2 -2
  10. package/dist/plugins/blog/client/hooks/index.d.cts +2 -2
  11. package/dist/plugins/blog/client/hooks/index.d.mts +2 -2
  12. package/dist/plugins/blog/client/hooks/index.d.ts +2 -2
  13. package/dist/plugins/blog/client/index.d.cts +2 -2
  14. package/dist/plugins/blog/client/index.d.mts +2 -2
  15. package/dist/plugins/blog/client/index.d.ts +2 -2
  16. package/dist/plugins/blog/query-keys.d.cts +2 -2
  17. package/dist/plugins/blog/query-keys.d.mts +2 -2
  18. package/dist/plugins/blog/query-keys.d.ts +2 -2
  19. package/dist/plugins/cms/api/index.d.cts +1 -1
  20. package/dist/plugins/cms/api/index.d.mts +1 -1
  21. package/dist/plugins/cms/api/index.d.ts +1 -1
  22. package/dist/plugins/cms/query-keys.d.cts +1 -1
  23. package/dist/plugins/cms/query-keys.d.mts +1 -1
  24. package/dist/plugins/cms/query-keys.d.ts +1 -1
  25. package/dist/shared/{stack.DOZ1EXjM.d.mts → stack.6mEHS2WH.d.mts} +3 -3
  26. package/dist/shared/{stack.DX-tQ93o.d.cts → stack.AJTXI7kw.d.cts} +3 -3
  27. package/dist/shared/{stack.E17kSK1W.d.mts → stack.C1jLzFZ6.d.mts} +5 -5
  28. package/dist/shared/{stack.D0p6oNme.d.ts → stack.DF1ewpVE.d.ts} +5 -5
  29. package/dist/shared/{stack.BX7MHi0J.d.mts → stack.DbnCLdNF.d.mts} +34 -7
  30. package/dist/shared/{stack.VF6FhyZw.d.ts → stack.QYn-Px94.d.ts} +3 -3
  31. package/dist/shared/{stack.DWipT53I.d.cts → stack.XulPeAD3.d.cts} +5 -5
  32. package/dist/shared/{stack.fQjVhw5a.d.ts → stack.fXlShnhv.d.ts} +34 -7
  33. package/dist/shared/{stack.C9zoS1TN.d.cts → stack.r6HohbyX.d.cts} +34 -7
  34. package/package.json +4 -4
  35. package/src/plugins/cms/api/index.ts +1 -0
  36. package/src/plugins/cms/api/mutations.ts +48 -3
  37. package/src/plugins/cms/api/plugin.ts +11 -101
  38. package/src/plugins/cms/api/relations.ts +174 -0
  39. package/dist/shared/{stack.BWp0hcm9.d.cts → stack.BQmuNl5p.d.cts} +3 -3
  40. package/dist/shared/{stack.BWp0hcm9.d.mts → stack.BQmuNl5p.d.mts} +3 -3
  41. package/dist/shared/{stack.BWp0hcm9.d.ts → stack.BQmuNl5p.d.ts} +3 -3
@@ -0,0 +1,174 @@
1
+ import type { DBAdapter as Adapter } from "@btst/db";
2
+ import type {
3
+ ContentType,
4
+ ContentRelation,
5
+ RelationConfig,
6
+ RelationValue,
7
+ } from "../types";
8
+
9
+ /**
10
+ * Shape of a property inside a content type's stored JSON Schema.
11
+ * Relation fields carry `fieldType: "relation"` and a `relation` descriptor.
12
+ */
13
+ interface JsonSchemaProperty {
14
+ fieldType?: string;
15
+ relation?: RelationConfig;
16
+ type?: string;
17
+ items?: JsonSchemaProperty;
18
+ [key: string]: unknown;
19
+ }
20
+
21
+ interface JsonSchemaWithProperties {
22
+ properties?: Record<string, JsonSchemaProperty>;
23
+ [key: string]: unknown;
24
+ }
25
+
26
+ /**
27
+ * Extract relation field configurations from a content type's JSON Schema.
28
+ *
29
+ * Returns a map of field name -> RelationConfig for every field declared
30
+ * with `fieldType: "relation"` and a populated `relation` descriptor.
31
+ */
32
+ export function extractRelationFields(
33
+ contentType: ContentType,
34
+ ): Record<string, RelationConfig> {
35
+ const jsonSchema = JSON.parse(
36
+ contentType.jsonSchema,
37
+ ) as JsonSchemaWithProperties;
38
+ const properties = jsonSchema.properties || {};
39
+ const relationFields: Record<string, RelationConfig> = {};
40
+
41
+ for (const [fieldName, fieldSchema] of Object.entries(properties)) {
42
+ if (fieldSchema.fieldType === "relation" && fieldSchema.relation) {
43
+ relationFields[fieldName] = fieldSchema.relation;
44
+ }
45
+ }
46
+
47
+ return relationFields;
48
+ }
49
+
50
+ /**
51
+ * Type guard: value is a `{ _new: true, data: ... }` descriptor used by the
52
+ * HTTP endpoints to request inline creation of a related item on save.
53
+ */
54
+ export function isNewRelationValue(
55
+ value: unknown,
56
+ ): value is { _new: true; data: Record<string, unknown> } {
57
+ return (
58
+ typeof value === "object" &&
59
+ value !== null &&
60
+ "_new" in value &&
61
+ (value as { _new: unknown })._new === true &&
62
+ "data" in value
63
+ );
64
+ }
65
+
66
+ /**
67
+ * Type guard: value is an existing relation reference (`{ id: string }`).
68
+ */
69
+ export function isExistingRelationValue(
70
+ value: unknown,
71
+ ): value is { id: string } {
72
+ return (
73
+ typeof value === "object" &&
74
+ value !== null &&
75
+ "id" in value &&
76
+ typeof (value as { id: unknown }).id === "string"
77
+ );
78
+ }
79
+
80
+ /**
81
+ * Collect relation target IDs out of a content item's data payload,
82
+ * grouped by relation field name.
83
+ *
84
+ * Only processes fields that are:
85
+ * - present in `data`
86
+ * - declared as relations in `relationFields`
87
+ * - hold existing `{ id }` references (or arrays of them)
88
+ *
89
+ * Unlike `processRelationsInData` in the HTTP route, this does NOT create
90
+ * inline `_new` items — callers passing `_new` values will have them
91
+ * silently ignored. Programmatic callers (seeds, imports) are expected to
92
+ * supply pre-created IDs.
93
+ *
94
+ * Returns a map of `{ fieldName: targetIds[] }` ready to hand to
95
+ * {@link syncRelations}.
96
+ */
97
+ export function collectExistingRelationIds(
98
+ data: Record<string, unknown>,
99
+ relationFields: Record<string, RelationConfig>,
100
+ ): Record<string, string[]> {
101
+ const relationIds: Record<string, string[]> = {};
102
+
103
+ for (const [fieldName, relationConfig] of Object.entries(relationFields)) {
104
+ if (!(fieldName in data)) continue;
105
+
106
+ const fieldValue = data[fieldName];
107
+ if (!fieldValue) {
108
+ relationIds[fieldName] = [];
109
+ continue;
110
+ }
111
+
112
+ const ids: string[] = [];
113
+
114
+ if (relationConfig.type === "belongsTo") {
115
+ const value = fieldValue as RelationValue;
116
+ if (isExistingRelationValue(value)) {
117
+ ids.push(value.id);
118
+ }
119
+ } else {
120
+ // hasMany / manyToMany — expect an array of { id }
121
+ const values = (
122
+ Array.isArray(fieldValue) ? fieldValue : []
123
+ ) as RelationValue[];
124
+
125
+ for (const value of values) {
126
+ if (isExistingRelationValue(value)) {
127
+ ids.push(value.id);
128
+ }
129
+ }
130
+ }
131
+
132
+ relationIds[fieldName] = ids;
133
+ }
134
+
135
+ return relationIds;
136
+ }
137
+
138
+ /**
139
+ * Sync relations in the junction table for a content item.
140
+ *
141
+ * Only updates relations for fields explicitly present in `relationIds`.
142
+ * Fields not included are left untouched — this preserves existing
143
+ * relations during partial updates.
144
+ *
145
+ * For each included field, existing junction rows (matching sourceId +
146
+ * fieldName) are deleted and replaced with the provided target IDs.
147
+ */
148
+ export async function syncRelations(
149
+ adapter: Adapter,
150
+ sourceId: string,
151
+ relationIds: Record<string, string[]>,
152
+ ): Promise<void> {
153
+ for (const [fieldName, targetIds] of Object.entries(relationIds)) {
154
+ await adapter.delete({
155
+ model: "contentRelation",
156
+ where: [
157
+ { field: "sourceId", value: sourceId, operator: "eq" as const },
158
+ { field: "fieldName", value: fieldName, operator: "eq" as const },
159
+ ],
160
+ });
161
+
162
+ for (const targetId of targetIds) {
163
+ await adapter.create<ContentRelation>({
164
+ model: "contentRelation",
165
+ data: {
166
+ sourceId,
167
+ targetId,
168
+ fieldName,
169
+ createdAt: new Date(),
170
+ },
171
+ });
172
+ }
173
+ }
174
+ }
@@ -42,15 +42,15 @@ declare const createPostSchema: z.ZodObject<{
42
42
  name: z.ZodString;
43
43
  slug: z.ZodString;
44
44
  }, z.core.$strip>]>>>>;
45
+ title: z.ZodString;
45
46
  slug: z.ZodOptional<z.ZodString>;
46
- published: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
47
- publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
48
47
  createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
49
48
  updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
50
- title: z.ZodString;
49
+ publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
51
50
  content: z.ZodString;
52
51
  excerpt: z.ZodString;
53
52
  image: z.ZodOptional<z.ZodString>;
53
+ published: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
54
54
  }, z.core.$strip>;
55
55
  declare const updatePostSchema: z.ZodObject<{
56
56
  publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
@@ -42,15 +42,15 @@ declare const createPostSchema: z.ZodObject<{
42
42
  name: z.ZodString;
43
43
  slug: z.ZodString;
44
44
  }, z.core.$strip>]>>>>;
45
+ title: z.ZodString;
45
46
  slug: z.ZodOptional<z.ZodString>;
46
- published: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
47
- publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
48
47
  createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
49
48
  updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
50
- title: z.ZodString;
49
+ publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
51
50
  content: z.ZodString;
52
51
  excerpt: z.ZodString;
53
52
  image: z.ZodOptional<z.ZodString>;
53
+ published: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
54
54
  }, z.core.$strip>;
55
55
  declare const updatePostSchema: z.ZodObject<{
56
56
  publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
@@ -42,15 +42,15 @@ declare const createPostSchema: z.ZodObject<{
42
42
  name: z.ZodString;
43
43
  slug: z.ZodString;
44
44
  }, z.core.$strip>]>>>>;
45
+ title: z.ZodString;
45
46
  slug: z.ZodOptional<z.ZodString>;
46
- published: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
47
- publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
48
47
  createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
49
48
  updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
50
- title: z.ZodString;
49
+ publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
51
50
  content: z.ZodString;
52
51
  excerpt: z.ZodString;
53
52
  image: z.ZodOptional<z.ZodString>;
53
+ published: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
54
54
  }, z.core.$strip>;
55
55
  declare const updatePostSchema: z.ZodObject<{
56
56
  publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;