@platforma-sdk/model 1.54.13 → 1.56.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 (120) hide show
  1. package/dist/bconfig/normalization.cjs +8 -1
  2. package/dist/bconfig/normalization.cjs.map +1 -1
  3. package/dist/bconfig/normalization.d.ts.map +1 -1
  4. package/dist/bconfig/normalization.js +8 -1
  5. package/dist/bconfig/normalization.js.map +1 -1
  6. package/dist/block_api_v3.d.ts +2 -2
  7. package/dist/block_api_v3.d.ts.map +1 -1
  8. package/dist/block_migrations.cjs +246 -214
  9. package/dist/block_migrations.cjs.map +1 -1
  10. package/dist/block_migrations.d.ts +180 -158
  11. package/dist/block_migrations.d.ts.map +1 -1
  12. package/dist/block_migrations.js +247 -214
  13. package/dist/block_migrations.js.map +1 -1
  14. package/dist/block_model.cjs +85 -35
  15. package/dist/block_model.cjs.map +1 -1
  16. package/dist/block_model.d.ts +66 -38
  17. package/dist/block_model.d.ts.map +1 -1
  18. package/dist/block_model.js +86 -36
  19. package/dist/block_model.js.map +1 -1
  20. package/dist/{builder.cjs → block_model_legacy.cjs} +2 -2
  21. package/dist/block_model_legacy.cjs.map +1 -0
  22. package/dist/{builder.d.ts → block_model_legacy.d.ts} +1 -1
  23. package/dist/block_model_legacy.d.ts.map +1 -0
  24. package/dist/{builder.js → block_model_legacy.js} +2 -2
  25. package/dist/block_model_legacy.js.map +1 -0
  26. package/dist/block_state_patch.d.ts +11 -1
  27. package/dist/block_state_patch.d.ts.map +1 -1
  28. package/dist/block_storage.cjs +126 -109
  29. package/dist/block_storage.cjs.map +1 -1
  30. package/dist/block_storage.d.ts +109 -112
  31. package/dist/block_storage.d.ts.map +1 -1
  32. package/dist/block_storage.js +126 -101
  33. package/dist/block_storage.js.map +1 -1
  34. package/dist/block_storage_callbacks.cjs +227 -0
  35. package/dist/block_storage_callbacks.cjs.map +1 -0
  36. package/dist/block_storage_callbacks.d.ts +113 -0
  37. package/dist/block_storage_callbacks.d.ts.map +1 -0
  38. package/dist/block_storage_callbacks.js +218 -0
  39. package/dist/block_storage_callbacks.js.map +1 -0
  40. package/dist/block_storage_facade.cjs +104 -0
  41. package/dist/block_storage_facade.cjs.map +1 -0
  42. package/dist/block_storage_facade.d.ts +168 -0
  43. package/dist/block_storage_facade.d.ts.map +1 -0
  44. package/dist/block_storage_facade.js +99 -0
  45. package/dist/block_storage_facade.js.map +1 -0
  46. package/dist/index.cjs +13 -14
  47. package/dist/index.cjs.map +1 -1
  48. package/dist/index.d.ts +8 -3
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +6 -4
  51. package/dist/index.js.map +1 -1
  52. package/dist/package.json.cjs +1 -1
  53. package/dist/package.json.js +1 -1
  54. package/dist/platforma.d.ts +11 -4
  55. package/dist/platforma.d.ts.map +1 -1
  56. package/dist/plugin_model.cjs +171 -0
  57. package/dist/plugin_model.cjs.map +1 -0
  58. package/dist/plugin_model.d.ts +162 -0
  59. package/dist/plugin_model.d.ts.map +1 -0
  60. package/dist/plugin_model.js +169 -0
  61. package/dist/plugin_model.js.map +1 -0
  62. package/dist/render/api.cjs +20 -21
  63. package/dist/render/api.cjs.map +1 -1
  64. package/dist/render/api.d.ts +8 -8
  65. package/dist/render/api.d.ts.map +1 -1
  66. package/dist/render/api.js +20 -21
  67. package/dist/render/api.js.map +1 -1
  68. package/dist/render/internal.cjs.map +1 -1
  69. package/dist/render/internal.d.ts +2 -1
  70. package/dist/render/internal.d.ts.map +1 -1
  71. package/dist/render/internal.js.map +1 -1
  72. package/dist/version.cjs +4 -0
  73. package/dist/version.cjs.map +1 -1
  74. package/dist/version.d.ts +4 -0
  75. package/dist/version.d.ts.map +1 -1
  76. package/dist/version.js +4 -1
  77. package/dist/version.js.map +1 -1
  78. package/package.json +5 -5
  79. package/src/bconfig/normalization.ts +8 -1
  80. package/src/block_api_v3.ts +2 -2
  81. package/src/block_migrations.test.ts +141 -171
  82. package/src/block_migrations.ts +300 -285
  83. package/src/block_model.ts +205 -95
  84. package/src/{builder.ts → block_model_legacy.ts} +1 -1
  85. package/src/block_state_patch.ts +13 -1
  86. package/src/block_storage.test.ts +283 -95
  87. package/src/block_storage.ts +199 -188
  88. package/src/block_storage_callbacks.ts +326 -0
  89. package/src/block_storage_facade.ts +199 -0
  90. package/src/index.ts +7 -3
  91. package/src/platforma.ts +26 -7
  92. package/src/plugin_model.test.ts +168 -0
  93. package/src/plugin_model.ts +242 -0
  94. package/src/render/api.ts +26 -24
  95. package/src/render/internal.ts +3 -1
  96. package/src/typing.test.ts +1 -1
  97. package/src/version.ts +8 -0
  98. package/dist/block_storage_vm.cjs +0 -262
  99. package/dist/block_storage_vm.cjs.map +0 -1
  100. package/dist/block_storage_vm.d.ts +0 -59
  101. package/dist/block_storage_vm.d.ts.map +0 -1
  102. package/dist/block_storage_vm.js +0 -258
  103. package/dist/block_storage_vm.js.map +0 -1
  104. package/dist/branding.d.ts +0 -7
  105. package/dist/branding.d.ts.map +0 -1
  106. package/dist/builder.cjs.map +0 -1
  107. package/dist/builder.d.ts.map +0 -1
  108. package/dist/builder.js.map +0 -1
  109. package/dist/sdk_info.cjs +0 -10
  110. package/dist/sdk_info.cjs.map +0 -1
  111. package/dist/sdk_info.d.ts +0 -5
  112. package/dist/sdk_info.d.ts.map +0 -1
  113. package/dist/sdk_info.js +0 -8
  114. package/dist/sdk_info.js.map +0 -1
  115. package/dist/unionize.d.ts +0 -12
  116. package/dist/unionize.d.ts.map +0 -1
  117. package/src/block_storage_vm.ts +0 -346
  118. package/src/branding.ts +0 -4
  119. package/src/sdk_info.ts +0 -9
  120. package/src/unionize.ts +0 -12
@@ -1,39 +1,5 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import {
3
- DataModelBuilder,
4
- defaultRecover,
5
- defineDataVersions,
6
- makeDataVersioned,
7
- } from "./block_migrations";
8
-
9
- describe("defineDataVersions", () => {
10
- it("throws on duplicate version values", () => {
11
- expect(() =>
12
- defineDataVersions({
13
- V1: "v1",
14
- V2: "v1", // duplicate!
15
- }),
16
- ).toThrow("Duplicate version values: v1");
17
- });
18
-
19
- it("throws on empty version values", () => {
20
- expect(() =>
21
- defineDataVersions({
22
- V1: "v1",
23
- V2: "", // empty!
24
- }),
25
- ).toThrow("Version values must be non-empty strings (empty: V2)");
26
- });
27
-
28
- it("allows unique version values", () => {
29
- const versions = defineDataVersions({
30
- V1: "v1",
31
- V2: "v2",
32
- });
33
- expect(versions.V1).toBe("v1");
34
- expect(versions.V2).toBe("v2");
35
- });
36
- });
2
+ import { DataModelBuilder, defaultRecover, makeDataVersioned } from "./block_migrations";
37
3
 
38
4
  describe("makeDataVersioned", () => {
39
5
  it("creates correct DataVersioned shape", () => {
@@ -44,19 +10,9 @@ describe("makeDataVersioned", () => {
44
10
 
45
11
  describe("DataModel migrations", () => {
46
12
  it("resets to initial data on unknown version", () => {
47
- const Version = defineDataVersions({
48
- V1: "v1",
49
- V2: "v2",
50
- });
51
-
52
- type VersionedData = {
53
- [Version.V1]: { count: number };
54
- [Version.V2]: { count: number; label: string };
55
- };
56
-
57
- const dataModel = new DataModelBuilder<VersionedData>()
58
- .from(Version.V1)
59
- .migrate(Version.V2, (v1) => ({ ...v1, label: "" }))
13
+ const dataModel = new DataModelBuilder()
14
+ .from<{ count: number }>("v1")
15
+ .migrate<{ count: number; label: string }>("v2", (v1) => ({ ...v1, label: "" }))
60
16
  .init(() => ({ count: 0, label: "" }));
61
17
 
62
18
  const result = dataModel.migrate(makeDataVersioned("legacy", { count: 42 }));
@@ -65,141 +21,155 @@ describe("DataModel migrations", () => {
65
21
  expect(result.warning).toBe(`Unknown version 'legacy'`);
66
22
  });
67
23
 
68
- it("uses recover() for unknown versions", () => {
69
- const Version = defineDataVersions({
70
- V1: "v1",
71
- V2: "v2",
72
- });
24
+ it("throws at build time on duplicate version key", () => {
25
+ expect(() =>
26
+ new DataModelBuilder()
27
+ .from<{ count: number }>("v1")
28
+ .migrate<{ count: number; label: string }>("v2", (v1) => ({ ...v1, label: "" }))
29
+ .migrate<{ count: number; label: string; description: string }>("v1", (v2) => ({
30
+ ...v2,
31
+ description: "",
32
+ })),
33
+ ).toThrow("Duplicate version 'v1' in migration chain");
34
+ });
73
35
 
74
- type VersionedData = {
75
- [Version.V1]: { count: number };
76
- [Version.V2]: { count: number; label: string };
77
- };
78
-
79
- const dataModel = new DataModelBuilder<VersionedData>()
80
- .from(Version.V1)
81
- .migrate(Version.V2, (v1) => ({ ...v1, label: "" }))
82
- .recover((version, data) => {
83
- if (version === "legacy" && typeof data === "object" && data !== null && "count" in data) {
84
- return { count: (data as { count: number }).count, label: "recovered" };
85
- }
86
- return defaultRecover(version, data);
36
+ it("returns initial data on migration failure", () => {
37
+ const dataModel = new DataModelBuilder()
38
+ .from<{ numbers: number[] }>("v1")
39
+ .migrate<{ numbers: number[]; label: string }>("v2", (v1) => {
40
+ if (v1.numbers.includes(666)) throw new Error("Forbidden number");
41
+ return { ...v1, label: "ok" };
87
42
  })
88
- .init(() => ({ count: 0, label: "" }));
43
+ .init(() => ({ numbers: [], label: "" }));
89
44
 
90
- const result = dataModel.migrate(makeDataVersioned("legacy", { count: 7 }));
45
+ const result = dataModel.migrate(makeDataVersioned("v1", { numbers: [666] }));
91
46
  expect(result.version).toBe("v2");
92
- expect(result.data).toStrictEqual({ count: 7, label: "recovered" });
93
- expect(result.warning).toBeUndefined();
47
+ expect(result.data).toStrictEqual({ numbers: [], label: "" });
48
+ expect(result.warning).toBe(`Migration v1→v2 failed: Forbidden number`);
94
49
  });
95
50
 
96
- it("allows recover() to delegate to defaultRecover", () => {
97
- const Version = defineDataVersions({
98
- V1: "v1",
99
- V2: "v2",
51
+ describe("recover()", () => {
52
+ it("recover() after from() — handles unknown version before any migrations run", () => {
53
+ const dataModel = new DataModelBuilder()
54
+ .from<{ count: number }>("v1")
55
+ .recover((version, data) => {
56
+ if (
57
+ version === "legacy" &&
58
+ typeof data === "object" &&
59
+ data !== null &&
60
+ "count" in data
61
+ ) {
62
+ return { count: (data as { count: number }).count };
63
+ }
64
+ return defaultRecover(version, data);
65
+ })
66
+ .migrate<{ count: number; label: string }>("v2", (v1) => ({ ...v1, label: "default" }))
67
+ .init(() => ({ count: 0, label: "" }));
68
+
69
+ // Legacy data is recovered as V1 then goes through v1→v2 migration
70
+ const result = dataModel.migrate(makeDataVersioned("legacy", { count: 5 }));
71
+ expect(result.version).toBe("v2");
72
+ expect(result.data).toStrictEqual({ count: 5, label: "default" });
73
+ expect(result.warning).toBeUndefined();
100
74
  });
101
75
 
102
- type VersionedData = {
103
- [Version.V1]: { count: number };
104
- [Version.V2]: { count: number; label: string };
105
- };
106
-
107
- const dataModel = new DataModelBuilder<VersionedData>()
108
- .from(Version.V1)
109
- .migrate(Version.V2, (v1) => ({ ...v1, label: "" }))
110
- .recover((version, data) => defaultRecover(version, data))
111
- .init(() => ({ count: 0, label: "" }));
76
+ it("recover() between migrations — recovered data goes through subsequent migrations", () => {
77
+ type V2 = { count: number; label: string };
78
+ type V3 = { count: number; label: string; description: string };
79
+
80
+ const dataModel = new DataModelBuilder()
81
+ .from<{ count: number }>("v1")
82
+ .migrate<V2>("v2", (v1) => ({ ...v1, label: "" }))
83
+ .recover((version, data) => {
84
+ if (
85
+ version === "legacy" &&
86
+ typeof data === "object" &&
87
+ data !== null &&
88
+ "count" in data
89
+ ) {
90
+ return { count: (data as { count: number }).count, label: "recovered" };
91
+ }
92
+ return defaultRecover(version, data);
93
+ })
94
+ .migrate<V3>("v3", (v2) => ({ ...v2, description: "added" }))
95
+ .init(() => ({ count: 0, label: "", description: "" }));
96
+
97
+ const result = dataModel.migrate(makeDataVersioned("legacy", { count: 7 }));
98
+ expect(result.version).toBe("v3");
99
+ expect(result.data).toStrictEqual({ count: 7, label: "recovered", description: "added" });
100
+ expect(result.warning).toBeUndefined();
101
+ });
112
102
 
113
- const result = dataModel.migrate(makeDataVersioned("legacy", { count: 7 }));
114
- expect(result.version).toBe("v2");
115
- expect(result.data).toStrictEqual({ count: 0, label: "" });
116
- expect(result.warning).toBe(`Unknown version 'legacy'`);
117
- });
103
+ it("recover() at the end of chain — recovered data is the final type", () => {
104
+ type V2 = { count: number; label: string };
105
+
106
+ const dataModel = new DataModelBuilder()
107
+ .from<{ count: number }>("v1")
108
+ .migrate<V2>("v2", (v1) => ({ ...v1, label: "" }))
109
+ .recover((version, data) => {
110
+ if (
111
+ version === "legacy" &&
112
+ typeof data === "object" &&
113
+ data !== null &&
114
+ "count" in data
115
+ ) {
116
+ return { count: (data as { count: number }).count, label: "recovered" };
117
+ }
118
+ return defaultRecover(version, data);
119
+ })
120
+ .init(() => ({ count: 0, label: "" }));
121
+
122
+ const result = dataModel.migrate(makeDataVersioned("legacy", { count: 9 }));
123
+ expect(result.version).toBe("v2");
124
+ expect(result.data).toStrictEqual({ count: 9, label: "recovered" });
125
+ expect(result.warning).toBeUndefined();
126
+ });
118
127
 
119
- it("returns initial data on migration failure", () => {
120
- const Version = defineDataVersions({
121
- V1: "v1",
122
- V2: "v2",
128
+ it("recover() delegates to defaultRecover for truly unknown versions", () => {
129
+ const dataModel = new DataModelBuilder()
130
+ .from<{ count: number }>("v1")
131
+ .migrate<{ count: number; label: string }>("v2", (v1) => ({ ...v1, label: "" }))
132
+ .recover((version, data) => defaultRecover(version, data))
133
+ .init(() => ({ count: 0, label: "" }));
134
+
135
+ const result = dataModel.migrate(makeDataVersioned("unknown", { count: 7 }));
136
+ expect(result.version).toBe("v2");
137
+ expect(result.data).toStrictEqual({ count: 0, label: "" });
138
+ expect(result.warning).toBe(`Unknown version 'unknown'`);
123
139
  });
124
140
 
125
- type VersionedData = {
126
- [Version.V1]: { numbers: number[] };
127
- [Version.V2]: { numbers: number[]; label: string };
128
- };
129
-
130
- const dataModel = new DataModelBuilder<VersionedData>()
131
- .from(Version.V1)
132
- .migrate(Version.V2, (v1) => {
133
- if (v1.numbers.includes(666)) {
134
- throw new Error("Forbidden number");
135
- }
136
- return { ...v1, label: "ok" };
137
- })
138
- .init(() => ({ numbers: [], label: "" }));
141
+ it("migration failure after recover() resets to initial data", () => {
142
+ type V2 = { count: number; label: string };
143
+ type V3 = { count: number; label: string; description: string };
144
+
145
+ const dataModel = new DataModelBuilder()
146
+ .from<{ count: number }>("v1")
147
+ .migrate<V2>("v2", (v1) => ({ ...v1, label: "" }))
148
+ .recover((version, data) => {
149
+ if (
150
+ version === "legacy" &&
151
+ typeof data === "object" &&
152
+ data !== null &&
153
+ "count" in data
154
+ ) {
155
+ return { count: (data as { count: number }).count, label: "recovered" };
156
+ }
157
+ return defaultRecover(version, data);
158
+ })
159
+ .migrate<V3>("v3", (_v2) => {
160
+ throw new Error("v3 failed");
161
+ })
162
+ .init(() => ({ count: 0, label: "", description: "" }));
163
+
164
+ const result = dataModel.migrate(makeDataVersioned("legacy", { count: 7 }));
165
+ expect(result.version).toBe("v3");
166
+ expect(result.data).toStrictEqual({ count: 0, label: "", description: "" });
167
+ expect(result.warning).toBe("Migration v2→v3 failed: v3 failed");
168
+ });
139
169
 
140
- const result = dataModel.migrate(makeDataVersioned("v1", { numbers: [666] }));
141
- expect(result.version).toBe("v2");
142
- expect(result.data).toStrictEqual({ numbers: [], label: "" });
143
- expect(result.warning).toBe(`Migration v1→v2 failed: Forbidden number`);
170
+ it("recover() cannot be called twice — enforced by type (no recover() on WithRecover)", () => {
171
+ // This is a compile-time-only check — WithRecover has no recover() method.
172
+ // Verified by the absence of recover() in DataModelMigrationChainWithRecover.
173
+ });
144
174
  });
145
175
  });
146
-
147
- function _compileTimeTypeChecks() {
148
- const Version = defineDataVersions({
149
- V1: "v1",
150
- V2: "v2",
151
- });
152
-
153
- type VersionedData = {
154
- [Version.V1]: { count: number };
155
- [Version.V2]: { count: number; label: string };
156
- };
157
-
158
- // Valid: complete migration chain
159
- new DataModelBuilder<VersionedData>()
160
- .from(Version.V1)
161
- .migrate(Version.V2, (v1) => ({ ...v1, label: "" }))
162
- .init(() => ({ count: 0, label: "" }));
163
-
164
- // Valid: with recover()
165
- new DataModelBuilder<VersionedData>()
166
- .from(Version.V1)
167
- .migrate(Version.V2, (v1) => ({ ...v1, label: "" }))
168
- .recover((version, data) => defaultRecover(version, data))
169
- .init(() => ({ count: 0, label: "" }));
170
-
171
- new DataModelBuilder<VersionedData>()
172
- // @ts-expect-error invalid initial version key
173
- .from("v3");
174
-
175
- new DataModelBuilder<VersionedData>()
176
- .from(Version.V1)
177
- // @ts-expect-error invalid migration target key
178
- .migrate("v3", (v1) => ({ ...v1, label: "" }));
179
-
180
- new DataModelBuilder<VersionedData>()
181
- .from(Version.V1)
182
- // @ts-expect-error migration return type must match target version
183
- .migrate(Version.V2, (v1) => ({ ...v1, invalid: true }));
184
-
185
- // Incomplete migration chain - V2 not covered
186
- // This errors at compile-time with the `this` parameter constraint:
187
- // "The 'this' context of type 'DataModelMigrationChain<..., "v1", "v2">' is not assignable to method's 'this' of type 'DataModelMigrationChain<..., "v1", never>'"
188
- // Note: @ts-expect-error doesn't work reliably in unused functions
189
- // new DataModelBuilder<VersionedData>()
190
- // .from(Version.V1)
191
- // .init(() => ({ count: 0 }));
192
-
193
- new DataModelBuilder<VersionedData>()
194
- .from(Version.V1)
195
- .migrate(Version.V2, (v1) => ({ ...v1, label: "" }))
196
- .recover((version, data) => defaultRecover(version, data))
197
- // @ts-expect-error recover() returns builder without recover() method - cannot call twice (only init() available)
198
- .recover((version, data) => defaultRecover(version, data));
199
-
200
- new DataModelBuilder<VersionedData>()
201
- .from(Version.V1)
202
- .recover((version, data) => defaultRecover(version, data))
203
- // @ts-expect-error recover() returns builder without migrate() method (only init() available)
204
- .migrate(Version.V2, (v1) => ({ ...v1, label: "" }));
205
- }