@ng-org/orm 0.1.2-alpha.1

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 (54) hide show
  1. package/README.md +347 -0
  2. package/dist/connector/applyPatches.d.ts +106 -0
  3. package/dist/connector/applyPatches.d.ts.map +1 -0
  4. package/dist/connector/applyPatches.js +317 -0
  5. package/dist/connector/applyPatches.test.d.ts +2 -0
  6. package/dist/connector/applyPatches.test.d.ts.map +1 -0
  7. package/dist/connector/applyPatches.test.js +772 -0
  8. package/dist/connector/createSignalObjectForShape.d.ts +14 -0
  9. package/dist/connector/createSignalObjectForShape.d.ts.map +1 -0
  10. package/dist/connector/createSignalObjectForShape.js +24 -0
  11. package/dist/connector/initNg.d.ts +14 -0
  12. package/dist/connector/initNg.d.ts.map +1 -0
  13. package/dist/connector/initNg.js +16 -0
  14. package/dist/connector/ormConnectionHandler.d.ts +47 -0
  15. package/dist/connector/ormConnectionHandler.d.ts.map +1 -0
  16. package/dist/connector/ormConnectionHandler.js +240 -0
  17. package/dist/frontendAdapters/react/index.d.ts +3 -0
  18. package/dist/frontendAdapters/react/index.d.ts.map +1 -0
  19. package/dist/frontendAdapters/react/index.js +2 -0
  20. package/dist/frontendAdapters/react/useShape.d.ts +6 -0
  21. package/dist/frontendAdapters/react/useShape.d.ts.map +1 -0
  22. package/dist/frontendAdapters/react/useShape.js +24 -0
  23. package/dist/frontendAdapters/svelte/index.d.ts +3 -0
  24. package/dist/frontendAdapters/svelte/index.d.ts.map +1 -0
  25. package/dist/frontendAdapters/svelte/index.js +2 -0
  26. package/dist/frontendAdapters/svelte/useShape.svelte.d.ts +14 -0
  27. package/dist/frontendAdapters/svelte/useShape.svelte.d.ts.map +1 -0
  28. package/dist/frontendAdapters/svelte/useShape.svelte.js +22 -0
  29. package/dist/frontendAdapters/vue/index.d.ts +3 -0
  30. package/dist/frontendAdapters/vue/index.d.ts.map +1 -0
  31. package/dist/frontendAdapters/vue/index.js +2 -0
  32. package/dist/frontendAdapters/vue/useShape.d.ts +5 -0
  33. package/dist/frontendAdapters/vue/useShape.d.ts.map +1 -0
  34. package/dist/frontendAdapters/vue/useShape.js +22 -0
  35. package/dist/index.d.ts +8 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +7 -0
  38. package/dist/types.d.ts +13 -0
  39. package/dist/types.d.ts.map +1 -0
  40. package/dist/types.js +10 -0
  41. package/package.json +90 -0
  42. package/src/connector/applyPatches.test.ts +842 -0
  43. package/src/connector/applyPatches.ts +404 -0
  44. package/src/connector/createSignalObjectForShape.ts +35 -0
  45. package/src/connector/initNg.ts +30 -0
  46. package/src/connector/ormConnectionHandler.ts +300 -0
  47. package/src/frontendAdapters/react/index.ts +2 -0
  48. package/src/frontendAdapters/react/useShape.ts +36 -0
  49. package/src/frontendAdapters/svelte/index.ts +2 -0
  50. package/src/frontendAdapters/svelte/useShape.svelte.ts +46 -0
  51. package/src/frontendAdapters/vue/index.ts +2 -0
  52. package/src/frontendAdapters/vue/useShape.ts +33 -0
  53. package/src/index.ts +14 -0
  54. package/src/types.ts +26 -0
@@ -0,0 +1,404 @@
1
+ // Copyright (c) 2025 Laurin Weger, Par le Peuple, NextGraph.org developers
2
+ // All rights reserved.
3
+ // Licensed under the Apache License, Version 2.0
4
+ // <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
5
+ // or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
6
+ // at your option. All files in the project carrying such
7
+ // notice may not be copied, modified, or distributed except
8
+ // according to those terms.
9
+ // SPDX-License-Identifier: Apache-2.0 OR MIT
10
+
11
+ import { batch } from "@ng-org/alien-deepsignals";
12
+
13
+ export type Patch = {
14
+ /** Property path (array indices, object keys, synthetic Set entry ids) from the root to the mutated location. */
15
+ path: string;
16
+ valType?: string & {};
17
+ value?: unknown;
18
+ } & (
19
+ | SetAddPatch
20
+ | SetRemovePatch
21
+ | ObjectAddPatch
22
+ | RemovePatch
23
+ | LiteralAddPatch
24
+ );
25
+
26
+ export interface SetAddPatch {
27
+ /** Mutation kind applied at the resolved `path`. */
28
+ op: "add";
29
+ valType: "set";
30
+ /**
31
+ * New value for set mutations:
32
+ * - A single primitive
33
+ * - An array of primitives
34
+ */
35
+ value: number | string | boolean | (number | string | boolean)[];
36
+ }
37
+
38
+ export interface SetRemovePatch {
39
+ /** Mutation kind applied at the resolved `path`. */
40
+ op: "remove";
41
+ valType: "set";
42
+ /**
43
+ * The value(s) to be removed from the set. Either:
44
+ * - A single primitive
45
+ * - An array of primitives
46
+ */
47
+ value: number | string | boolean | (number | string | boolean)[];
48
+ }
49
+
50
+ export interface ObjectAddPatch {
51
+ /** Mutation kind applied at the resolved `path`. */
52
+ op: "add";
53
+ valType: "object";
54
+ }
55
+
56
+ export interface RemovePatch {
57
+ /** Mutation kind applied at the resolved `path`. */
58
+ op: "remove";
59
+ }
60
+
61
+ export interface LiteralAddPatch {
62
+ /** Mutation kind applied at the resolved `path`. */
63
+ op: "add";
64
+ /** The literal value to be added at the resolved `path` */
65
+ value: string | number | boolean;
66
+ }
67
+
68
+ function isPrimitive(v: unknown): v is string | number | boolean {
69
+ return (
70
+ typeof v === "string" || typeof v === "number" || typeof v === "boolean"
71
+ );
72
+ }
73
+
74
+ /**
75
+ * Parse a combined identifier of the form "graph|subject".
76
+ * If there is no pipe, returns { graph: undefined, id: input }.
77
+ */
78
+ function parseGraphId(input: string): { graph?: string; id: string } {
79
+ if (typeof input !== "string") return { id: String(input) } as any;
80
+ const idx = input.indexOf("|");
81
+ if (idx === -1) return { id: input };
82
+ const graph = input.slice(0, idx);
83
+ const id = input.slice(idx + 1);
84
+ return { graph, id };
85
+ }
86
+
87
+ /**
88
+ * Find an object in a Set by its @id property.
89
+ * Returns the object if found, otherwise undefined.
90
+ */
91
+ function findInSetBySegment(set: Set<any>, seg: string): any | undefined {
92
+ // TODO: We could optimize that by leveraging key @id to object mapping in sets of deepSignals.
93
+
94
+ const { graph, id } = parseGraphId(seg);
95
+
96
+ for (const item of set) {
97
+ if (typeof item !== "object" || item === null) continue;
98
+ // If graph was provided, require both to match
99
+ if (graph && item["@graph"] === graph && item["@id"] === id)
100
+ return item;
101
+ // Match by @id only when no graph part is provided
102
+ if (!graph && item["@id"] === id) return item;
103
+ }
104
+ return undefined;
105
+ }
106
+
107
+ /**
108
+ * Apply a diff to an object.
109
+ *
110
+ * The syntax is inspired by RFC 6902 but it is not compatible.
111
+ *
112
+ * It supports Sets for multi-valued properties:
113
+ * - Primitive values are added as Sets (Set<string | number | boolean>)
114
+ * - Multi-valued objects are stored in Sets, accessed by their @id property
115
+ * - Single objects are plain objects with an @id property
116
+ *
117
+ * Path traversal:
118
+ * - When traversing through a Set, the path segment is treated as an @id to find the object
119
+ * - When traversing through a plain object, the path segment is a property name
120
+ *
121
+ * @example operations
122
+ * ```jsonc
123
+ * // === SINGLE OBJECT ===
124
+ * // Creating a single object (has @id at same level)
125
+ * { "op": "add", "path": "/urn:example:person1/address", "valType": "object" }
126
+ * { "op": "add", "path": "/urn:example:person1/address/@id", "value": "urn:test:address1" }
127
+ * // Adding primitives to single object
128
+ * { "op": "add", "path": "/urn:example:person1/address/street", "value": "1st street" }
129
+ * { "op": "add", "path": "/urn:example:person1/address/country", "value": "Greece" }
130
+ * // Remove a primitive from object
131
+ * { "op": "remove", "path": "/urn:example:person1/address/street" }
132
+ * // Remove the entire object
133
+ * { "op": "remove", "path": "/urn:example:person1/address" }
134
+ *
135
+ * // === MULTI-VALUED OBJECTS (Set) ===
136
+ * // Creating a multi-object container (NO @id at this level -> creates Set)
137
+ * { "op": "add", "path": "/urn:example:person1/children", "valType": "object" }
138
+ * // Adding an object to the Set (path includes object's @id)
139
+ * { "op": "add", "path": "/urn:example:person1/children/urn:example:child1", "valType": "object" }
140
+ * { "op": "add", "path": "/urn:example:person1/children/urn:example:child1/@id", "value": "urn:example:child1" }
141
+ * // Adding properties to object in Set
142
+ * { "op": "add", "path": "/urn:example:person1/children/urn:example:child1/name", "value": "Alice" }
143
+ * // Remove an object from Set
144
+ * { "op": "remove", "path": "/urn:example:person1/children/urn:example:child1" }
145
+ * // Remove all objects (the Set itself)
146
+ * { "op": "remove", "path": "/urn:example:person1/children" }
147
+ *
148
+ * // === PRIMITIVE SETS ===
149
+ * // Add primitive types to Sets
150
+ * { "op": "add", "valType": "set", "path": "/urn:example:person1/tags", "value": [1,2,3] }
151
+ * // Remove primitive types from a Set
152
+ * { "op": "remove", "valType": "set", "path": "/urn:example:person1/tags", "value": [1,2] }
153
+ * ```
154
+ *
155
+ * @param currentState The object before the patch
156
+ * @param patches An array of patches to apply to the object.
157
+ * @param ensurePathExists If true, create nested objects along the path if the path does not exist.
158
+ *
159
+ * @note When creating new objects, this function pre-scans upcoming patches to find @id and @graph
160
+ * values that will be assigned to the object. This prevents the signal library's propGenerator
161
+ * from being triggered before these identity fields are set, which would cause it to generate
162
+ * random IDs unnecessarily.
163
+ */
164
+ export function applyPatches(
165
+ currentState: Record<string, any>,
166
+ patches: Patch[],
167
+ ensurePathExists: boolean = false
168
+ ) {
169
+ for (let patchIndex = 0; patchIndex < patches.length; patchIndex++) {
170
+ const patch = patches[patchIndex];
171
+ if (!patch.path.startsWith("/")) continue;
172
+ const pathParts = patch.path
173
+ .slice(1)
174
+ .split("/")
175
+ .filter(Boolean)
176
+ .map(decodePathSegment);
177
+
178
+ if (pathParts.length === 0) {
179
+ console.warn("[applyDiff] No path specified for patch", patch);
180
+ continue;
181
+ }
182
+ const lastKey = pathParts[pathParts.length - 1];
183
+ let parentVal: any = currentState;
184
+ let parentMissing = false;
185
+ // Traverse only intermediate segments (to leaf object at path)
186
+ for (let i = 0; i < pathParts.length - 1; i++) {
187
+ const seg = pathParts[i];
188
+ // Handle Sets: if parentVal is a Set, find object by path segment.
189
+ if (parentVal instanceof Set) {
190
+ const foundObj = findInSetBySegment(parentVal, seg);
191
+ if (foundObj) {
192
+ parentVal = foundObj;
193
+ } else if (ensurePathExists) {
194
+ // Create new object in the set.
195
+ const newObj = {};
196
+ parentVal.add(newObj);
197
+ parentVal = newObj;
198
+ } else {
199
+ parentMissing = true;
200
+ break;
201
+ }
202
+ continue;
203
+ }
204
+
205
+ // Handle regular objects
206
+ if (
207
+ parentVal != null &&
208
+ typeof parentVal === "object" &&
209
+ Object.prototype.hasOwnProperty.call(parentVal, seg)
210
+ ) {
211
+ parentVal = parentVal[seg];
212
+ continue;
213
+ }
214
+ if (ensurePathExists) {
215
+ if (parentVal != null && typeof parentVal === "object") {
216
+ // Check if we need to create an object or a set:
217
+ if (pathParts[i + 1]?.includes("|")) {
218
+ // The next path segment is an IRI, that means the new element must be a set of objects. Create a set.
219
+ parentVal[seg] = new Set();
220
+ } else {
221
+ // Create a new object
222
+ parentVal[seg] = {};
223
+ }
224
+ parentVal = parentVal[seg];
225
+ } else {
226
+ parentMissing = true;
227
+ break;
228
+ }
229
+ } else {
230
+ parentMissing = true;
231
+ break;
232
+ }
233
+ }
234
+
235
+ if (parentMissing) {
236
+ console.warn(
237
+ `[applyDiff] Skipping patch due to missing parent path segment(s): ${patch.path}`
238
+ );
239
+ continue;
240
+ }
241
+
242
+ // parentVal now should be an object or Set into which we apply lastKey
243
+ if (
244
+ parentVal == null ||
245
+ (typeof parentVal !== "object" && !(parentVal instanceof Set))
246
+ ) {
247
+ console.warn(
248
+ `[applyDiff] Skipping patch because parent is not an object or Set: ${patch.path}`
249
+ );
250
+ continue;
251
+ }
252
+ const key = lastKey;
253
+
254
+ // Special handling when parent is a Set
255
+ if (parentVal instanceof Set) {
256
+ // The key represents the identifier of an object within the Set
257
+ const targetObj = findInSetBySegment(parentVal, key);
258
+
259
+ // Handle object creation in a Set
260
+ if (patch.op === "add" && patch.valType === "object") {
261
+ if (!targetObj) {
262
+ // Determine if this will be a single object or nested Set
263
+ const hasId = patches[patchIndex + 2]?.path.endsWith("@id");
264
+ const newLeaf: any = hasId ? {} : new Set();
265
+ // Pre-assign identity so subsequent patches can find this object
266
+ if (hasId) {
267
+ const { graph, id } = parseGraphId(key);
268
+ newLeaf["@id"] = id;
269
+ const graphPatch = patches[patchIndex + 1];
270
+ if (graphPatch?.path.endsWith("@graph")) {
271
+ newLeaf["@graph"] = graphPatch.value ?? graph;
272
+ } else if (graph) {
273
+ newLeaf["@graph"] = graph;
274
+ }
275
+ }
276
+ parentVal.add(newLeaf);
277
+ }
278
+ continue;
279
+ }
280
+
281
+ // Handle remove from Set
282
+ if (patch.op === "remove" && patch.valType !== "set") {
283
+ if (targetObj) {
284
+ parentVal.delete(targetObj);
285
+ }
286
+ continue;
287
+ }
288
+
289
+ // All other operations require the target object to exist
290
+ if (!targetObj) {
291
+ console.warn(
292
+ `[applyDiff] Target object with @id=${key} not found in Set for path: ${patch.path}`
293
+ );
294
+ continue;
295
+ }
296
+
297
+ // This shouldn't happen - we handle all intermediate segments in the traversal loop
298
+ console.warn(
299
+ `[applyDiff] Unexpected: reached end of path with Set as parent: ${patch.path}`
300
+ );
301
+ continue;
302
+ }
303
+
304
+ // Regular object handling (parentVal is a plain object, not a Set)
305
+ // Handle primitive set additions
306
+ if (patch.op === "add" && patch.valType === "set") {
307
+ const existing = parentVal[key];
308
+ const raw = (patch as SetAddPatch).value;
309
+ if (raw == null) continue;
310
+
311
+ // Normalize to array of primitives
312
+ const toAdd: (string | number | boolean)[] = Array.isArray(raw)
313
+ ? raw.filter(isPrimitive)
314
+ : isPrimitive(raw)
315
+ ? [raw]
316
+ : [];
317
+
318
+ if (!toAdd.length) continue;
319
+
320
+ // Ensure we have a Set, create or add to existing
321
+ if (existing instanceof Set) {
322
+ for (const v of toAdd) existing.add(v);
323
+ } else {
324
+ // Create new Set (replaces any incompatible existing value)
325
+ parentVal[key] = new Set(toAdd);
326
+ }
327
+ continue;
328
+ }
329
+
330
+ // Handle primitive set removals
331
+ if (patch.op === "remove" && patch.valType === "set") {
332
+ const existing = parentVal[key];
333
+ const raw = (patch as SetRemovePatch).value;
334
+ if (raw == null) continue;
335
+
336
+ const toRemove: (string | number | boolean)[] = Array.isArray(raw)
337
+ ? raw
338
+ : [raw];
339
+
340
+ if (existing instanceof Set) {
341
+ for (const v of toRemove) existing.delete(v);
342
+ }
343
+ continue;
344
+ }
345
+
346
+ // Add object (if it does not exist yet).
347
+ // Distinguish between single objects and multi-object containers:
348
+ // - If an @id patch follows for this path, it's a single object -> create {}
349
+ // - If no @id patch follows, it's a container for multi-valued objects -> create set.
350
+ if (patch.op === "add" && patch.valType === "object") {
351
+ const leafVal = parentVal[key];
352
+ const hasId = patches.at(patchIndex + 2)?.path.endsWith("@id");
353
+
354
+ // If the leafVal does not exist and it should be a set, create.
355
+ if (!hasId && !leafVal) {
356
+ parentVal[key] = new Set();
357
+ } else if (!(typeof leafVal === "object")) {
358
+ // If the leave does not exist yet (as object), create it.
359
+ const newLeaf: Record<string, any> = {};
360
+ const graphPatch = patches.at(patchIndex + 1);
361
+ if (graphPatch?.path.endsWith("@graph")) {
362
+ newLeaf["@graph"] = graphPatch.value;
363
+ }
364
+ const idPatch = patches.at(patchIndex + 2);
365
+ if (idPatch?.path.endsWith("@id")) {
366
+ newLeaf["@id"] = idPatch.value;
367
+ }
368
+ parentVal[key] = newLeaf;
369
+ }
370
+
371
+ continue;
372
+ }
373
+
374
+ // Literal add
375
+ if (
376
+ patch.op === "add" &&
377
+ !(patch.path.endsWith("@id") || patch.path.endsWith("@graph"))
378
+ ) {
379
+ parentVal[key] = (patch as LiteralAddPatch).value;
380
+ continue;
381
+ }
382
+
383
+ // Generic remove (property or value)
384
+ if (patch.op === "remove") {
385
+ if (Object.prototype.hasOwnProperty.call(parentVal, key)) {
386
+ delete parentVal[key];
387
+ }
388
+ continue;
389
+ }
390
+ }
391
+ }
392
+
393
+ /**
394
+ * See documentation for applyDiff
395
+ */
396
+ export function applyPatchesToDeepSignal(currentState: object, patch: Patch[]) {
397
+ batch(() => {
398
+ applyPatches(currentState as Record<string, any>, patch, true);
399
+ });
400
+ }
401
+
402
+ function decodePathSegment(segment: string): string {
403
+ return segment.replace("~1", "/").replace("~0", "~");
404
+ }
@@ -0,0 +1,35 @@
1
+ // Copyright (c) 2025 Laurin Weger, Par le Peuple, NextGraph.org developers
2
+ // All rights reserved.
3
+ // Licensed under the Apache License, Version 2.0
4
+ // <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
5
+ // or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
6
+ // at your option. All files in the project carrying such
7
+ // notice may not be copied, modified, or distributed except
8
+ // according to those terms.
9
+ // SPDX-License-Identifier: Apache-2.0 OR MIT
10
+
11
+ import type { Diff, Scope } from "../types.ts";
12
+ import type { ShapeType, BaseType } from "@ng-org/shex-orm";
13
+ import { OrmConnection } from "./ormConnectionHandler.ts";
14
+
15
+ /**
16
+ *
17
+ * @param shapeType
18
+ * @param scope
19
+ * @returns
20
+ */
21
+ export function createSignalObjectForShape<T extends BaseType>(
22
+ shapeType: ShapeType<T>,
23
+ scope?: Scope
24
+ ) {
25
+ const connection: OrmConnection<T> = OrmConnection.getConnection(
26
+ shapeType,
27
+ scope || ""
28
+ );
29
+
30
+ return {
31
+ signalObject: connection.signalObject,
32
+ stop: connection.release,
33
+ readyPromise: connection.readyPromise,
34
+ };
35
+ }
@@ -0,0 +1,30 @@
1
+ // Copyright (c) 2025 Laurin Weger, Par le Peuple, NextGraph.org developers
2
+ // All rights reserved.
3
+ // Licensed under the Apache License, Version 2.0
4
+ // <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
5
+ // or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
6
+ // at your option. All files in the project carrying such
7
+ // notice may not be copied, modified, or distributed except
8
+ // according to those terms.
9
+ // SPDX-License-Identifier: Apache-2.0 OR MIT
10
+
11
+ import * as NG from "@ng-org/lib-wasm";
12
+
13
+ type Session = {
14
+ session_id: string | number;
15
+ protected_store_id: string;
16
+ private_store_id: string;
17
+ public_store_id: string;
18
+ };
19
+
20
+ let resolveNgSession: (value: { ng: typeof NG; session: Session }) => void;
21
+
22
+ export const ngSession = new Promise<{ ng: typeof NG; session: Session }>(
23
+ (resolve) => {
24
+ resolveNgSession = resolve;
25
+ }
26
+ );
27
+
28
+ export function initNgSignals(ngImpl: typeof NG, session: Session) {
29
+ resolveNgSession({ ng: ngImpl, session });
30
+ }