fabricjs-document-engine 0.7.0 → 1.0.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 (39) hide show
  1. package/README.md +45 -91
  2. package/dist/assets/asset-pipeline.cjs +3 -1
  3. package/dist/assets/asset-pipeline.js +3 -1
  4. package/dist/engine/create-document-engine.cjs +16 -6
  5. package/dist/engine/create-document-engine.d.cts +7 -1
  6. package/dist/engine/create-document-engine.d.ts +7 -1
  7. package/dist/engine/create-document-engine.js +17 -7
  8. package/dist/engine/errors.d.cts +1 -1
  9. package/dist/engine/errors.d.ts +1 -1
  10. package/dist/export/render-export.cjs +3 -15
  11. package/dist/export/render-export.js +4 -15
  12. package/dist/history/apply-state.cjs +3 -1
  13. package/dist/history/apply-state.js +3 -1
  14. package/dist/history/create-history.cjs +4 -1
  15. package/dist/history/create-history.d.cts +1 -0
  16. package/dist/history/create-history.d.ts +1 -0
  17. package/dist/history/create-history.js +4 -1
  18. package/dist/history/history-stack.cjs +44 -8
  19. package/dist/history/history-stack.js +44 -9
  20. package/dist/index.cjs +4 -0
  21. package/dist/index.d.cts +3 -2
  22. package/dist/index.d.ts +3 -2
  23. package/dist/index.js +2 -1
  24. package/dist/recovery/recovery-controller.cjs +30 -3
  25. package/dist/recovery/recovery-controller.js +30 -3
  26. package/dist/security/content-limits.cjs +86 -0
  27. package/dist/security/content-limits.d.cts +11 -0
  28. package/dist/security/content-limits.d.ts +11 -0
  29. package/dist/security/content-limits.js +84 -0
  30. package/dist/storage/verify-storage-adapter.cjs +125 -0
  31. package/dist/storage/verify-storage-adapter.d.cts +13 -0
  32. package/dist/storage/verify-storage-adapter.d.ts +13 -0
  33. package/dist/storage/verify-storage-adapter.js +125 -0
  34. package/dist/storage.cjs +2 -0
  35. package/dist/storage.d.cts +2 -1
  36. package/dist/storage.d.ts +2 -1
  37. package/dist/storage.js +2 -1
  38. package/package.json +5 -2
  39. package/schema/document-v1.schema.json +69 -0
@@ -0,0 +1,125 @@
1
+ require("../document/document-format.cjs");
2
+ const require_ids = require("../document/ids.cjs");
3
+ const require_document_version = require("../versions/document-version.cjs");
4
+ //#region src/storage/verify-storage-adapter.ts
5
+ function sampleDocument(id, revision, label) {
6
+ const now = (/* @__PURE__ */ new Date()).toISOString();
7
+ return {
8
+ schemaVersion: 1,
9
+ id,
10
+ createdAt: now,
11
+ updatedAt: now,
12
+ revision,
13
+ canvas: {
14
+ width: 100,
15
+ height: 100
16
+ },
17
+ objects: [{
18
+ type: "Rect",
19
+ id: `${id}-shape`,
20
+ width: 10,
21
+ height: 10,
22
+ fill: label
23
+ }],
24
+ metadata: { label }
25
+ };
26
+ }
27
+ function codeOf(error) {
28
+ return error?.code;
29
+ }
30
+ async function verifyStorageAdapter(storage) {
31
+ const checks = [];
32
+ const documentId = `adapter-check-${require_ids.createId()}`;
33
+ const signal = new AbortController().signal;
34
+ let revision = 0;
35
+ async function check(name, work) {
36
+ try {
37
+ const problem = await work();
38
+ checks.push(problem === void 0 ? {
39
+ name,
40
+ passed: true
41
+ } : {
42
+ name,
43
+ passed: false,
44
+ message: problem
45
+ });
46
+ return problem === void 0;
47
+ } catch (error) {
48
+ checks.push({
49
+ name,
50
+ passed: false,
51
+ message: error instanceof Error ? error.message : String(error)
52
+ });
53
+ return false;
54
+ }
55
+ }
56
+ async function saveAt(expectedRevision, label) {
57
+ return (await storage.saveDocument(sampleDocument(documentId, (expectedRevision ?? revision) + 1, label), {
58
+ expectedRevision,
59
+ signal
60
+ }))?.revision ?? (expectedRevision ?? revision) + 1;
61
+ }
62
+ if (await check("saves a new document when expectedRevision is 0", async () => {
63
+ revision = await saveAt(0, "first");
64
+ return revision > 0 ? void 0 : `returned revision ${revision}, expected a number above 0`;
65
+ })) {
66
+ await check("loads the document it saved", async () => {
67
+ const loaded = await storage.loadDocument(documentId);
68
+ if (loaded?.id !== documentId) return `loaded id ${String(loaded?.id)}, expected ${documentId}`;
69
+ if (loaded.metadata?.label !== "first") return "loaded content differs from what was saved";
70
+ });
71
+ await check("refuses a save based on an old revision with code SAVE_CONFLICT", async () => {
72
+ try {
73
+ await saveAt(revision - 1, "stale");
74
+ } catch (error) {
75
+ return codeOf(error) === "SAVE_CONFLICT" ? void 0 : `rejected with code ${String(codeOf(error))} instead of SAVE_CONFLICT`;
76
+ }
77
+ return "the stale save was accepted, so an older tab could overwrite newer work";
78
+ });
79
+ await check("accepts a save based on the current revision", async () => {
80
+ const next = await saveAt(revision, "second");
81
+ if (next <= revision) return `revision went from ${revision} to ${next}, it must increase`;
82
+ revision = next;
83
+ return (await storage.loadDocument(documentId))?.metadata?.label === "second" ? void 0 : "the newer content was not stored";
84
+ });
85
+ await check("overwrites when expectedRevision is null", async () => {
86
+ revision = await saveAt(null, "overwritten");
87
+ return (await storage.loadDocument(documentId))?.metadata?.label === "overwritten" ? void 0 : "the overwrite was not stored";
88
+ });
89
+ }
90
+ await check("rejects loading a document that does not exist", async () => {
91
+ try {
92
+ const missing = await storage.loadDocument(`adapter-check-missing-${require_ids.createId()}`);
93
+ return missing === void 0 || missing === null ? "resolved with nothing instead of rejecting, which hides a missing document" : "returned a document for an id that was never saved";
94
+ } catch {
95
+ return;
96
+ }
97
+ });
98
+ if (require_document_version.supportsVersions(storage)) {
99
+ const version = {
100
+ id: `version-${require_ids.createId()}`,
101
+ documentId,
102
+ name: "Adapter check",
103
+ kind: "named",
104
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
105
+ revision,
106
+ document: sampleDocument(documentId, revision, "versioned")
107
+ };
108
+ await check("keeps, lists, loads and deletes versions", async () => {
109
+ await storage.saveVersion(version);
110
+ const listed = await storage.listVersions(documentId);
111
+ if (!listed.some((summary) => summary.id === version.id)) return "listVersions did not include the saved version";
112
+ if (listed.some((summary) => "document" in summary)) return "listVersions should return summaries without the document";
113
+ if ((await storage.loadVersion(documentId, version.id)).document.metadata.label !== "versioned") return "loadVersion returned different content";
114
+ await storage.deleteVersion(documentId, version.id);
115
+ return (await storage.listVersions(documentId)).some((summary) => summary.id === version.id) ? "deleteVersion did not remove the version" : void 0;
116
+ });
117
+ }
118
+ await storage.deleteDocument?.(documentId).catch(() => void 0);
119
+ return {
120
+ ok: checks.every((result) => result.passed),
121
+ checks
122
+ };
123
+ }
124
+ //#endregion
125
+ exports.verifyStorageAdapter = verifyStorageAdapter;
@@ -0,0 +1,13 @@
1
+ import { DocumentStorage } from "./storage-contract.cjs";
2
+ //#region src/storage/verify-storage-adapter.d.ts
3
+ export interface AdapterCheck {
4
+ name: string;
5
+ passed: boolean;
6
+ message?: string;
7
+ }
8
+ export interface AdapterReport {
9
+ ok: boolean;
10
+ checks: AdapterCheck[];
11
+ }
12
+ export declare function verifyStorageAdapter(storage: DocumentStorage): Promise<AdapterReport>;
13
+ //#endregion
@@ -0,0 +1,13 @@
1
+ import { DocumentStorage } from "./storage-contract.js";
2
+ //#region src/storage/verify-storage-adapter.d.ts
3
+ export interface AdapterCheck {
4
+ name: string;
5
+ passed: boolean;
6
+ message?: string;
7
+ }
8
+ export interface AdapterReport {
9
+ ok: boolean;
10
+ checks: AdapterCheck[];
11
+ }
12
+ export declare function verifyStorageAdapter(storage: DocumentStorage): Promise<AdapterReport>;
13
+ //#endregion
@@ -0,0 +1,125 @@
1
+ import "../document/document-format.js";
2
+ import { createId } from "../document/ids.js";
3
+ import { supportsVersions } from "../versions/document-version.js";
4
+ //#region src/storage/verify-storage-adapter.ts
5
+ function sampleDocument(id, revision, label) {
6
+ const now = (/* @__PURE__ */ new Date()).toISOString();
7
+ return {
8
+ schemaVersion: 1,
9
+ id,
10
+ createdAt: now,
11
+ updatedAt: now,
12
+ revision,
13
+ canvas: {
14
+ width: 100,
15
+ height: 100
16
+ },
17
+ objects: [{
18
+ type: "Rect",
19
+ id: `${id}-shape`,
20
+ width: 10,
21
+ height: 10,
22
+ fill: label
23
+ }],
24
+ metadata: { label }
25
+ };
26
+ }
27
+ function codeOf(error) {
28
+ return error?.code;
29
+ }
30
+ async function verifyStorageAdapter(storage) {
31
+ const checks = [];
32
+ const documentId = `adapter-check-${createId()}`;
33
+ const signal = new AbortController().signal;
34
+ let revision = 0;
35
+ async function check(name, work) {
36
+ try {
37
+ const problem = await work();
38
+ checks.push(problem === void 0 ? {
39
+ name,
40
+ passed: true
41
+ } : {
42
+ name,
43
+ passed: false,
44
+ message: problem
45
+ });
46
+ return problem === void 0;
47
+ } catch (error) {
48
+ checks.push({
49
+ name,
50
+ passed: false,
51
+ message: error instanceof Error ? error.message : String(error)
52
+ });
53
+ return false;
54
+ }
55
+ }
56
+ async function saveAt(expectedRevision, label) {
57
+ return (await storage.saveDocument(sampleDocument(documentId, (expectedRevision ?? revision) + 1, label), {
58
+ expectedRevision,
59
+ signal
60
+ }))?.revision ?? (expectedRevision ?? revision) + 1;
61
+ }
62
+ if (await check("saves a new document when expectedRevision is 0", async () => {
63
+ revision = await saveAt(0, "first");
64
+ return revision > 0 ? void 0 : `returned revision ${revision}, expected a number above 0`;
65
+ })) {
66
+ await check("loads the document it saved", async () => {
67
+ const loaded = await storage.loadDocument(documentId);
68
+ if (loaded?.id !== documentId) return `loaded id ${String(loaded?.id)}, expected ${documentId}`;
69
+ if (loaded.metadata?.label !== "first") return "loaded content differs from what was saved";
70
+ });
71
+ await check("refuses a save based on an old revision with code SAVE_CONFLICT", async () => {
72
+ try {
73
+ await saveAt(revision - 1, "stale");
74
+ } catch (error) {
75
+ return codeOf(error) === "SAVE_CONFLICT" ? void 0 : `rejected with code ${String(codeOf(error))} instead of SAVE_CONFLICT`;
76
+ }
77
+ return "the stale save was accepted, so an older tab could overwrite newer work";
78
+ });
79
+ await check("accepts a save based on the current revision", async () => {
80
+ const next = await saveAt(revision, "second");
81
+ if (next <= revision) return `revision went from ${revision} to ${next}, it must increase`;
82
+ revision = next;
83
+ return (await storage.loadDocument(documentId))?.metadata?.label === "second" ? void 0 : "the newer content was not stored";
84
+ });
85
+ await check("overwrites when expectedRevision is null", async () => {
86
+ revision = await saveAt(null, "overwritten");
87
+ return (await storage.loadDocument(documentId))?.metadata?.label === "overwritten" ? void 0 : "the overwrite was not stored";
88
+ });
89
+ }
90
+ await check("rejects loading a document that does not exist", async () => {
91
+ try {
92
+ const missing = await storage.loadDocument(`adapter-check-missing-${createId()}`);
93
+ return missing === void 0 || missing === null ? "resolved with nothing instead of rejecting, which hides a missing document" : "returned a document for an id that was never saved";
94
+ } catch {
95
+ return;
96
+ }
97
+ });
98
+ if (supportsVersions(storage)) {
99
+ const version = {
100
+ id: `version-${createId()}`,
101
+ documentId,
102
+ name: "Adapter check",
103
+ kind: "named",
104
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
105
+ revision,
106
+ document: sampleDocument(documentId, revision, "versioned")
107
+ };
108
+ await check("keeps, lists, loads and deletes versions", async () => {
109
+ await storage.saveVersion(version);
110
+ const listed = await storage.listVersions(documentId);
111
+ if (!listed.some((summary) => summary.id === version.id)) return "listVersions did not include the saved version";
112
+ if (listed.some((summary) => "document" in summary)) return "listVersions should return summaries without the document";
113
+ if ((await storage.loadVersion(documentId, version.id)).document.metadata.label !== "versioned") return "loadVersion returned different content";
114
+ await storage.deleteVersion(documentId, version.id);
115
+ return (await storage.listVersions(documentId)).some((summary) => summary.id === version.id) ? "deleteVersion did not remove the version" : void 0;
116
+ });
117
+ }
118
+ await storage.deleteDocument?.(documentId).catch(() => void 0);
119
+ return {
120
+ ok: checks.every((result) => result.passed),
121
+ checks
122
+ };
123
+ }
124
+ //#endregion
125
+ export { verifyStorageAdapter };
package/dist/storage.cjs CHANGED
@@ -2,6 +2,8 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_key_value_storage = require("./storage/key-value-storage.cjs");
3
3
  const require_memory_storage = require("./storage/memory-storage.cjs");
4
4
  const require_local_storage = require("./storage/local-storage.cjs");
5
+ const require_verify_storage_adapter = require("./storage/verify-storage-adapter.cjs");
5
6
  exports.createKeyValueStorage = require_key_value_storage.createKeyValueStorage;
6
7
  exports.createLocalStorage = require_local_storage.createLocalStorage;
7
8
  exports.createMemoryStorage = require_memory_storage.createMemoryStorage;
9
+ exports.verifyStorageAdapter = require_verify_storage_adapter.verifyStorageAdapter;
@@ -3,4 +3,5 @@ import { DocumentStorage, SaveContext, SaveResult } from "./storage/storage-cont
3
3
  import { KeyValueStore, ManagedDocumentStorage, createKeyValueStorage } from "./storage/key-value-storage.cjs";
4
4
  import { createMemoryStorage } from "./storage/memory-storage.cjs";
5
5
  import { LocalStorageOptions, createLocalStorage } from "./storage/local-storage.cjs";
6
- export { type DocumentStorage, type DocumentVersion, type KeyValueStore, type LocalStorageOptions, type ManagedDocumentStorage, type SaveContext, type SaveResult, type VersionStorage, type VersionSummary, createKeyValueStorage, createLocalStorage, createMemoryStorage };
6
+ import { AdapterCheck, AdapterReport, verifyStorageAdapter } from "./storage/verify-storage-adapter.cjs";
7
+ export { type AdapterCheck, type AdapterReport, type DocumentStorage, type DocumentVersion, type KeyValueStore, type LocalStorageOptions, type ManagedDocumentStorage, type SaveContext, type SaveResult, type VersionStorage, type VersionSummary, createKeyValueStorage, createLocalStorage, createMemoryStorage, verifyStorageAdapter };
package/dist/storage.d.ts CHANGED
@@ -3,4 +3,5 @@ import { DocumentStorage, SaveContext, SaveResult } from "./storage/storage-cont
3
3
  import { KeyValueStore, ManagedDocumentStorage, createKeyValueStorage } from "./storage/key-value-storage.js";
4
4
  import { createMemoryStorage } from "./storage/memory-storage.js";
5
5
  import { LocalStorageOptions, createLocalStorage } from "./storage/local-storage.js";
6
- export { type DocumentStorage, type DocumentVersion, type KeyValueStore, type LocalStorageOptions, type ManagedDocumentStorage, type SaveContext, type SaveResult, type VersionStorage, type VersionSummary, createKeyValueStorage, createLocalStorage, createMemoryStorage };
6
+ import { AdapterCheck, AdapterReport, verifyStorageAdapter } from "./storage/verify-storage-adapter.js";
7
+ export { type AdapterCheck, type AdapterReport, type DocumentStorage, type DocumentVersion, type KeyValueStore, type LocalStorageOptions, type ManagedDocumentStorage, type SaveContext, type SaveResult, type VersionStorage, type VersionSummary, createKeyValueStorage, createLocalStorage, createMemoryStorage, verifyStorageAdapter };
package/dist/storage.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { createKeyValueStorage } from "./storage/key-value-storage.js";
2
2
  import { createMemoryStorage } from "./storage/memory-storage.js";
3
3
  import { createLocalStorage } from "./storage/local-storage.js";
4
- export { createKeyValueStorage, createLocalStorage, createMemoryStorage };
4
+ import { verifyStorageAdapter } from "./storage/verify-storage-adapter.js";
5
+ export { createKeyValueStorage, createLocalStorage, createMemoryStorage, verifyStorageAdapter };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fabricjs-document-engine",
3
- "version": "0.7.0",
3
+ "version": "1.0.0",
4
4
  "description": "Turn an existing Fabric.js canvas into a dependable editable document: stable object ids, versioned save and load, undo and redo, safe autosave with conflict protection, custom objects and clear errors.",
5
5
  "keywords": [
6
6
  "fabric",
@@ -29,6 +29,7 @@
29
29
  "sideEffects": false,
30
30
  "files": [
31
31
  "dist",
32
+ "schema",
32
33
  "README.md",
33
34
  "LICENSE"
34
35
  ],
@@ -76,6 +77,7 @@
76
77
  "default": "./dist/react.cjs"
77
78
  }
78
79
  },
80
+ "./schema/document-v1.json": "./schema/document-v1.schema.json",
79
81
  "./package.json": "./package.json"
80
82
  },
81
83
  "scripts": {
@@ -84,7 +86,7 @@
84
86
  "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.tools.json",
85
87
  "test": "vitest run",
86
88
  "test:unit": "vitest run --project unit",
87
- "test:browser": "vitest run --project browser-fabric7 --project browser-fabric6",
89
+ "test:browser": "vitest run --project browser-fabric7 --project browser-fabric6 --project browser-firefox --project browser-webkit",
88
90
  "test:watch": "vitest",
89
91
  "lint:publint": "publint --strict",
90
92
  "lint:attw": "attw --pack . --profile node16",
@@ -117,6 +119,7 @@
117
119
  "@types/react": "^19.3.0",
118
120
  "@types/react-dom": "^19.3.0",
119
121
  "@vitest/browser-playwright": "^5.0.2",
122
+ "ajv": "^8.20.0",
120
123
  "fabric": "^7.4.0",
121
124
  "fabric6": "npm:fabric@^6.9.1",
122
125
  "playwright": "^1.63.0",
@@ -0,0 +1,69 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://unpkg.com/fabricjs-document-engine/schema/document-v1.schema.json",
4
+ "title": "Fabric document, schema version 1",
5
+ "description": "The document format written by fabricjs-document-engine 1.x. Fabric object properties are kept as Fabric serializes them.",
6
+ "type": "object",
7
+ "required": ["schemaVersion", "id", "createdAt", "updatedAt", "canvas", "objects", "metadata"],
8
+ "properties": {
9
+ "schemaVersion": { "const": 1 },
10
+ "id": { "type": "string", "minLength": 1 },
11
+ "createdAt": { "type": "string" },
12
+ "updatedAt": { "type": "string" },
13
+ "revision": { "type": "integer", "minimum": 0 },
14
+ "fabricVersion": { "type": "string" },
15
+ "canvas": {
16
+ "type": "object",
17
+ "required": ["width", "height"],
18
+ "properties": {
19
+ "width": { "type": "number", "exclusiveMinimum": 0 },
20
+ "height": { "type": "number", "exclusiveMinimum": 0 },
21
+ "background": {}
22
+ }
23
+ },
24
+ "objects": { "type": "array", "items": { "$ref": "#/$defs/fabricObject" } },
25
+ "assets": {
26
+ "type": "object",
27
+ "required": ["images", "fonts"],
28
+ "properties": {
29
+ "images": {
30
+ "type": "array",
31
+ "items": {
32
+ "type": "object",
33
+ "required": ["url", "objectIds"],
34
+ "properties": {
35
+ "url": { "type": "string" },
36
+ "objectIds": { "type": "array", "items": { "type": "string" } }
37
+ }
38
+ }
39
+ },
40
+ "fonts": {
41
+ "type": "array",
42
+ "items": {
43
+ "type": "object",
44
+ "required": ["family", "weight", "style", "objectIds"],
45
+ "properties": {
46
+ "family": { "type": "string" },
47
+ "weight": { "type": "string" },
48
+ "style": { "type": "string" },
49
+ "objectIds": { "type": "array", "items": { "type": "string" } }
50
+ }
51
+ }
52
+ }
53
+ }
54
+ },
55
+ "metadata": { "type": "object" }
56
+ },
57
+ "$defs": {
58
+ "fabricObject": {
59
+ "type": "object",
60
+ "required": ["type"],
61
+ "properties": {
62
+ "type": { "type": "string", "minLength": 1 },
63
+ "id": { "type": "string" },
64
+ "objects": { "type": "array", "items": { "$ref": "#/$defs/fabricObject" } },
65
+ "clipPath": { "$ref": "#/$defs/fabricObject" }
66
+ }
67
+ }
68
+ }
69
+ }