@batterai/strapi-plugin-canvas 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/LICENSE +16 -0
  3. package/README.md +131 -0
  4. package/dist/admin/appearance.d.ts +12 -0
  5. package/dist/admin/appearance.d.ts.map +1 -0
  6. package/dist/admin/appearance.js +44 -0
  7. package/dist/admin/appearance.js.map +1 -0
  8. package/dist/admin/block-packages.d.ts +4 -0
  9. package/dist/admin/block-packages.d.ts.map +1 -0
  10. package/dist/admin/block-packages.js +14 -0
  11. package/dist/admin/block-packages.js.map +1 -0
  12. package/dist/admin/client/content-model.d.ts +3 -0
  13. package/dist/admin/client/content-model.d.ts.map +1 -0
  14. package/dist/admin/client/content-model.js +76 -0
  15. package/dist/admin/client/content-model.js.map +1 -0
  16. package/dist/admin/client/host-services.d.ts +4 -0
  17. package/dist/admin/client/host-services.d.ts.map +1 -0
  18. package/dist/admin/client/host-services.js +141 -0
  19. package/dist/admin/client/host-services.js.map +1 -0
  20. package/dist/admin/index.d.ts +37 -0
  21. package/dist/admin/index.d.ts.map +1 -0
  22. package/dist/admin/index.js +34 -0
  23. package/dist/admin/index.js.map +1 -0
  24. package/dist/admin/pages/App.d.ts +4 -0
  25. package/dist/admin/pages/App.d.ts.map +1 -0
  26. package/dist/admin/pages/App.js +50 -0
  27. package/dist/admin/pages/App.js.map +1 -0
  28. package/dist/admin/translations.d.ts +4 -0
  29. package/dist/admin/translations.d.ts.map +1 -0
  30. package/dist/admin/translations.js +16 -0
  31. package/dist/admin/translations.js.map +1 -0
  32. package/dist/constants.d.ts +14 -0
  33. package/dist/constants.d.ts.map +1 -0
  34. package/dist/constants.js +18 -0
  35. package/dist/constants.js.map +1 -0
  36. package/dist/index.d.ts +2 -0
  37. package/dist/index.d.ts.map +1 -0
  38. package/dist/index.js +2 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/server/bootstrap.d.ts +4 -0
  41. package/dist/server/bootstrap.d.ts.map +1 -0
  42. package/dist/server/bootstrap.js +37 -0
  43. package/dist/server/bootstrap.js.map +1 -0
  44. package/dist/server/config/index.d.ts +24 -0
  45. package/dist/server/config/index.d.ts.map +1 -0
  46. package/dist/server/config/index.js +33 -0
  47. package/dist/server/config/index.js.map +1 -0
  48. package/dist/server/content-types/canvas-page/schema.d.ts +45 -0
  49. package/dist/server/content-types/canvas-page/schema.d.ts.map +1 -0
  50. package/dist/server/content-types/canvas-page/schema.js +21 -0
  51. package/dist/server/content-types/canvas-page/schema.js.map +1 -0
  52. package/dist/server/content-types/canvas-site/schema.d.ts +41 -0
  53. package/dist/server/content-types/canvas-site/schema.d.ts.map +1 -0
  54. package/dist/server/content-types/canvas-site/schema.js +20 -0
  55. package/dist/server/content-types/canvas-site/schema.js.map +1 -0
  56. package/dist/server/content-types/index.d.ts +90 -0
  57. package/dist/server/content-types/index.d.ts.map +1 -0
  58. package/dist/server/content-types/index.js +7 -0
  59. package/dist/server/content-types/index.js.map +1 -0
  60. package/dist/server/controllers/canvas.d.ts +48 -0
  61. package/dist/server/controllers/canvas.d.ts.map +1 -0
  62. package/dist/server/controllers/canvas.js +409 -0
  63. package/dist/server/controllers/canvas.js.map +1 -0
  64. package/dist/server/controllers/index.d.ts +448 -0
  65. package/dist/server/controllers/index.d.ts.map +1 -0
  66. package/dist/server/controllers/index.js +5 -0
  67. package/dist/server/controllers/index.js.map +1 -0
  68. package/dist/server/index.d.ts +645 -0
  69. package/dist/server/index.d.ts.map +1 -0
  70. package/dist/server/index.js +21 -0
  71. package/dist/server/index.js.map +1 -0
  72. package/dist/server/license.d.ts +13 -0
  73. package/dist/server/license.d.ts.map +1 -0
  74. package/dist/server/license.js +24 -0
  75. package/dist/server/license.js.map +1 -0
  76. package/dist/server/policies/is-admin-authenticated.d.ts +2 -0
  77. package/dist/server/policies/is-admin-authenticated.d.ts.map +1 -0
  78. package/dist/server/policies/is-admin-authenticated.js +4 -0
  79. package/dist/server/policies/is-admin-authenticated.js.map +1 -0
  80. package/dist/server/register.d.ts +2 -0
  81. package/dist/server/register.d.ts.map +1 -0
  82. package/dist/server/register.js +4 -0
  83. package/dist/server/register.js.map +1 -0
  84. package/dist/server/routes/admin.d.ts +18 -0
  85. package/dist/server/routes/admin.d.ts.map +1 -0
  86. package/dist/server/routes/admin.js +38 -0
  87. package/dist/server/routes/admin.js.map +1 -0
  88. package/dist/server/routes/index.d.ts +31 -0
  89. package/dist/server/routes/index.d.ts.map +1 -0
  90. package/dist/server/routes/index.js +7 -0
  91. package/dist/server/routes/index.js.map +1 -0
  92. package/dist/server/routes/public.d.ts +13 -0
  93. package/dist/server/routes/public.d.ts.map +1 -0
  94. package/dist/server/routes/public.js +7 -0
  95. package/dist/server/routes/public.js.map +1 -0
  96. package/dist/server/services/collaboration-service.d.ts +24 -0
  97. package/dist/server/services/collaboration-service.d.ts.map +1 -0
  98. package/dist/server/services/collaboration-service.js +164 -0
  99. package/dist/server/services/collaboration-service.js.map +1 -0
  100. package/dist/server/services/document-command-service.d.ts +34 -0
  101. package/dist/server/services/document-command-service.d.ts.map +1 -0
  102. package/dist/server/services/document-command-service.js +418 -0
  103. package/dist/server/services/document-command-service.js.map +1 -0
  104. package/dist/server/services/document-service.d.ts +14 -0
  105. package/dist/server/services/document-service.d.ts.map +1 -0
  106. package/dist/server/services/document-service.js +144 -0
  107. package/dist/server/services/document-service.js.map +1 -0
  108. package/dist/server/services/index.d.ts +68 -0
  109. package/dist/server/services/index.d.ts.map +1 -0
  110. package/dist/server/services/index.js +28 -0
  111. package/dist/server/services/index.js.map +1 -0
  112. package/dist/server/services/page-doc-store.d.ts +59 -0
  113. package/dist/server/services/page-doc-store.d.ts.map +1 -0
  114. package/dist/server/services/page-doc-store.js +285 -0
  115. package/dist/server/services/page-doc-store.js.map +1 -0
  116. package/dist/server/services/site-store.d.ts +43 -0
  117. package/dist/server/services/site-store.d.ts.map +1 -0
  118. package/dist/server/services/site-store.js +212 -0
  119. package/dist/server/services/site-store.js.map +1 -0
  120. package/dist/server-cjs/index.js +17381 -0
  121. package/dist/server-cjs/package.json +3 -0
  122. package/package.json +77 -0
  123. package/src/admin/appearance.ts +56 -0
  124. package/src/admin/block-packages.ts +16 -0
  125. package/src/admin/client/content-model.ts +88 -0
  126. package/src/admin/client/host-services.ts +172 -0
  127. package/src/admin/index.tsx +56 -0
  128. package/src/admin/pages/App.tsx +56 -0
  129. package/src/admin/strapi-admin.d.ts +1 -0
  130. package/src/admin/translations.ts +23 -0
  131. package/src/constants.ts +21 -0
  132. package/src/index.ts +11 -0
  133. package/src/server/bootstrap.ts +45 -0
  134. package/src/server/config/index.ts +46 -0
  135. package/src/server/content-types/canvas-page/schema.ts +20 -0
  136. package/src/server/content-types/canvas-site/schema.ts +19 -0
  137. package/src/server/content-types/index.ts +7 -0
  138. package/src/server/controllers/canvas.ts +468 -0
  139. package/src/server/controllers/index.ts +5 -0
  140. package/src/server/index.ts +21 -0
  141. package/src/server/license.ts +42 -0
  142. package/src/server/policies/is-admin-authenticated.ts +3 -0
  143. package/src/server/register.ts +3 -0
  144. package/src/server/routes/admin.ts +40 -0
  145. package/src/server/routes/index.ts +7 -0
  146. package/src/server/routes/public.ts +6 -0
  147. package/src/server/services/collaboration-service.ts +180 -0
  148. package/src/server/services/document-command-service.ts +495 -0
  149. package/src/server/services/document-service.ts +162 -0
  150. package/src/server/services/index.ts +41 -0
  151. package/src/server/services/page-doc-store.ts +348 -0
  152. package/src/server/services/site-store.ts +243 -0
@@ -0,0 +1,243 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import type { BlockLibrary, CanvasPage, EditorShellBootstrap, EditorShellContent } from "@batterai/editor-shell";
3
+ import { BATTERY_THEME_DEFAULT_TOKENS, type CanvasDocument } from "@batterai/contract";
4
+ import { CANVAS_SITE_UID, DEFAULT_SITE_KEY } from "../../constants";
5
+ import type { CanvasPluginConfig } from "../config";
6
+
7
+ export type CanvasSiteDocument = {
8
+ kind: "canvas-editor-site";
9
+ docVersion: number;
10
+ content: EditorShellContent;
11
+ contentTypes: EditorShellBootstrap["contentTypes"];
12
+ library: BlockLibrary;
13
+ pageOrder: string[];
14
+ canvas: Pick<CanvasDocument, "theme" | "assets" | "variables" | "layout">;
15
+ published?: {
16
+ document: CanvasDocument;
17
+ content: EditorShellContent;
18
+ library: BlockLibrary;
19
+ publishedAt: string;
20
+ sourceDocVersion: number;
21
+ };
22
+ status: EditorShellBootstrap["status"];
23
+ };
24
+
25
+ export class CanvasSiteConflict extends Error {
26
+ readonly status = 409;
27
+
28
+ constructor(
29
+ readonly expectedDocVersion: number,
30
+ readonly actualDocVersion: number,
31
+ ) {
32
+ super(`Canvas site conflict: expected ${expectedDocVersion}, got ${actualDocVersion}`);
33
+ this.name = "CanvasSiteConflict";
34
+ }
35
+ }
36
+
37
+ export function emptyLibrary(): BlockLibrary {
38
+ return { categories: [], blocks: [], presets: [], sharedBlocks: [] };
39
+ }
40
+
41
+ /**
42
+ * Normalizes a host-supplied `seedLibrary` plugin config value into a
43
+ * `BlockLibrary`, filling any missing keys with empty arrays. Returns
44
+ * `undefined` when no seed was configured so callers can fall back to
45
+ * `emptyLibrary()`.
46
+ */
47
+ export function normalizeLibrary(seed?: CanvasPluginConfig["seedLibrary"]): BlockLibrary | undefined {
48
+ if (!seed || typeof seed !== "object" || Array.isArray(seed)) return undefined;
49
+ return {
50
+ categories: Array.isArray(seed.categories) ? (seed.categories as BlockLibrary["categories"]) : [],
51
+ blocks: Array.isArray(seed.blocks) ? (seed.blocks as BlockLibrary["blocks"]) : [],
52
+ presets: Array.isArray(seed.presets) ? (seed.presets as BlockLibrary["presets"]) : [],
53
+ sharedBlocks: [],
54
+ };
55
+ }
56
+
57
+ export function defaultHomePage(_now = new Date()): CanvasPage {
58
+ return {
59
+ id: "home",
60
+ title: "Home",
61
+ path: "/",
62
+ parentId: null,
63
+ root: [],
64
+ };
65
+ }
66
+
67
+ export function defaultSiteDocument(now = new Date()): CanvasSiteDocument {
68
+ return {
69
+ kind: "canvas-editor-site",
70
+ docVersion: 1,
71
+ content: {
72
+ media: [],
73
+ routes: [{ id: "home", label: "Home", path: "/" }],
74
+ sharedBlocks: [],
75
+ siteMetadata: { title: "Strapi site" },
76
+ translations: {},
77
+ },
78
+ contentTypes: [],
79
+ library: emptyLibrary(),
80
+ pageOrder: ["home"],
81
+ canvas: {
82
+ theme: { tokens: BATTERY_THEME_DEFAULT_TOKENS },
83
+ assets: [],
84
+ variables: {},
85
+ layout: { pageOrder: ["home" as never] },
86
+ },
87
+ status: {
88
+ adapter: "strapi",
89
+ connected: true,
90
+ product: "Canvas",
91
+ runtimeModel: "strapi-admin-plugin",
92
+ source: "plugin::canvas.canvas-site",
93
+ version: "0.1.0",
94
+ lastSyncedAt: now.toISOString(),
95
+ },
96
+ };
97
+ }
98
+
99
+ export function createSiteStore(
100
+ strapi: any,
101
+ seedLibrary?: CanvasPluginConfig["seedLibrary"],
102
+ transaction?: ((tableName: string) => any),
103
+ ) {
104
+ const query = () => strapi.documents?.("plugin::canvas.canvas-site") ?? strapi.entityService;
105
+ const metadata = transaction ? strapi.db.metadata.get(CANVAS_SITE_UID) : undefined;
106
+ const tableName = metadata?.tableName;
107
+ const columns = metadata ? {
108
+ documentId: columnName(metadata, "documentId", "document_id"),
109
+ siteKey: columnName(metadata, "siteKey", "site_key"),
110
+ docVersion: columnName(metadata, "docVersion", "doc_version"),
111
+ document: columnName(metadata, "document", "document"),
112
+ createdAt: columnName(metadata, "createdAt", "created_at"),
113
+ updatedAt: columnName(metadata, "updatedAt", "updated_at"),
114
+ } : undefined;
115
+
116
+ function table() {
117
+ if (!transaction || !tableName) throw new Error("Canvas site aggregate writes require a Strapi transaction.");
118
+ return transaction(tableName);
119
+ }
120
+
121
+ async function transactionalRow() {
122
+ return table().where({ [columns!.siteKey]: DEFAULT_SITE_KEY }).first();
123
+ }
124
+
125
+ return {
126
+ async readOrCreate(): Promise<CanvasSiteDocument> {
127
+ if (transaction) {
128
+ const existing = await transactionalRow();
129
+ if (existing?.[columns!.document]) return normalizeSiteDocument(parseJson(existing[columns!.document]));
130
+ const document: CanvasSiteDocument = {
131
+ ...defaultSiteDocument(),
132
+ library: normalizeLibrary(seedLibrary) ?? emptyLibrary(),
133
+ };
134
+ const timestamp = new Date().toISOString();
135
+ await table().insert({
136
+ [columns!.documentId]: randomUUID(),
137
+ [columns!.siteKey]: DEFAULT_SITE_KEY,
138
+ [columns!.docVersion]: document.docVersion,
139
+ [columns!.document]: JSON.stringify(document),
140
+ [columns!.createdAt]: timestamp,
141
+ [columns!.updatedAt]: timestamp,
142
+ });
143
+ return document;
144
+ }
145
+ const rows = await strapi.entityService.findMany("plugin::canvas.canvas-site", {
146
+ filters: { siteKey: { $eq: DEFAULT_SITE_KEY } },
147
+ limit: 1,
148
+ });
149
+ const existing = rows[0];
150
+ if (existing?.document) return normalizeSiteDocument(existing.document);
151
+ const document: CanvasSiteDocument = {
152
+ ...defaultSiteDocument(),
153
+ library: normalizeLibrary(seedLibrary) ?? emptyLibrary(),
154
+ };
155
+ await query().create?.({
156
+ data: {
157
+ siteKey: DEFAULT_SITE_KEY,
158
+ docVersion: document.docVersion,
159
+ document,
160
+ },
161
+ });
162
+ return document;
163
+ },
164
+ async update(mutator: (document: CanvasSiteDocument) => CanvasSiteDocument): Promise<CanvasSiteDocument> {
165
+ const rows = await strapi.entityService.findMany("plugin::canvas.canvas-site", {
166
+ filters: { siteKey: { $eq: DEFAULT_SITE_KEY } },
167
+ limit: 1,
168
+ });
169
+ const existing = rows[0];
170
+ const next = mutator(normalizeSiteDocument(existing?.document));
171
+ if (existing?.documentId) {
172
+ await query().update?.({
173
+ documentId: existing.documentId,
174
+ data: { docVersion: next.docVersion, document: next },
175
+ });
176
+ }
177
+ return next;
178
+ },
179
+ async updateIfVersion(
180
+ expectedDocVersion: number,
181
+ mutator: (document: CanvasSiteDocument) => CanvasSiteDocument,
182
+ ): Promise<CanvasSiteDocument> {
183
+ const existing = await transactionalRow();
184
+ const current = normalizeSiteDocument(parseJson(existing?.[columns!.document]));
185
+ const actualDocVersion = Number(existing?.[columns!.docVersion] ?? current.docVersion);
186
+ if (!existing || actualDocVersion !== expectedDocVersion) {
187
+ throw new CanvasSiteConflict(expectedDocVersion, actualDocVersion);
188
+ }
189
+ const next = mutator(current);
190
+ if (next.docVersion !== expectedDocVersion + 1) {
191
+ throw new Error("Canvas site aggregate mutation must increment docVersion exactly once.");
192
+ }
193
+ const affected = await table()
194
+ .where({
195
+ [columns!.siteKey]: DEFAULT_SITE_KEY,
196
+ [columns!.docVersion]: expectedDocVersion,
197
+ })
198
+ .update({
199
+ [columns!.docVersion]: next.docVersion,
200
+ [columns!.document]: JSON.stringify(next),
201
+ [columns!.updatedAt]: new Date().toISOString(),
202
+ });
203
+ if (Number(affected) !== 1) {
204
+ const latest = await transactionalRow();
205
+ throw new CanvasSiteConflict(expectedDocVersion, Number(latest?.[columns!.docVersion] ?? actualDocVersion));
206
+ }
207
+ return next;
208
+ },
209
+ };
210
+ }
211
+
212
+ export function normalizeSiteDocument(document: unknown): CanvasSiteDocument {
213
+ if (!document || typeof document !== "object" || Array.isArray(document)) return defaultSiteDocument();
214
+ const candidate = document as Partial<CanvasSiteDocument>;
215
+ return {
216
+ ...defaultSiteDocument(),
217
+ ...candidate,
218
+ content: {
219
+ ...defaultSiteDocument().content,
220
+ ...(candidate.content ?? {}),
221
+ },
222
+ library: candidate.library ?? emptyLibrary(),
223
+ canvas: {
224
+ ...defaultSiteDocument().canvas,
225
+ ...(candidate.canvas ?? {}),
226
+ },
227
+ pageOrder: Array.isArray(candidate.pageOrder) ? candidate.pageOrder.filter(Boolean) : ["home"],
228
+ docVersion: Number.isInteger(candidate.docVersion) && candidate.docVersion! > 0 ? candidate.docVersion! : 1,
229
+ };
230
+ }
231
+
232
+ function columnName(metadata: any, attribute: string, fallback: string): string {
233
+ return metadata?.attributes?.[attribute]?.columnName ?? fallback;
234
+ }
235
+
236
+ function parseJson(value: unknown): unknown {
237
+ if (typeof value !== "string") return value;
238
+ try {
239
+ return JSON.parse(value);
240
+ } catch {
241
+ return value;
242
+ }
243
+ }