@sixb/core 0.1.2 → 0.1.3
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.
- package/dist/actions/index.js +16 -13
- package/dist/actions/index.js.map +1 -1
- package/dist/agents/adapters.d.ts +11 -1
- package/dist/agents/adapters.d.ts.map +1 -1
- package/dist/agents/context.js +1 -1
- package/dist/agents/errors.d.ts +1 -1
- package/dist/agents/errors.d.ts.map +1 -1
- package/dist/agents/execution.d.ts +3 -0
- package/dist/agents/execution.d.ts.map +1 -1
- package/dist/agents/index.d.ts +6 -3
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/index.js +33 -22
- package/dist/agents/index.js.map +1 -1
- package/dist/agents/prompt.d.ts +8 -2
- package/dist/agents/prompt.d.ts.map +1 -1
- package/dist/agents/request.d.ts.map +1 -1
- package/dist/agents/streams.js +2 -2
- package/dist/agents/thread-context-projection.d.ts +19 -0
- package/dist/agents/thread-context-projection.d.ts.map +1 -0
- package/dist/agents/tool-definition.d.ts.map +1 -1
- package/dist/agents/tool-result.d.ts +6 -0
- package/dist/agents/tool-result.d.ts.map +1 -0
- package/dist/agents/types.d.ts +37 -1
- package/dist/agents/types.d.ts.map +1 -1
- package/dist/agents/validation.d.ts +1 -1
- package/dist/agents/validation.d.ts.map +1 -1
- package/dist/auth/index.js +7 -5
- package/dist/auth/index.js.map +1 -1
- package/dist/authorization/index.js +1 -1
- package/dist/bootstrap/index.js +27 -24
- package/dist/bootstrap/index.js.map +1 -1
- package/dist/{chunk-0mbx247x.js → chunk-177ed2mp.js} +241 -45
- package/dist/chunk-177ed2mp.js.map +46 -0
- package/dist/{chunk-sa4340y1.js → chunk-1ywq4hzk.js} +7 -7
- package/dist/{chunk-zg9yryhz.js → chunk-261ag4t4.js} +3 -3
- package/dist/{chunk-k9pts48t.js → chunk-3rsf30p4.js} +5 -5
- package/dist/{chunk-8ryen3p3.js → chunk-4aw0jwk0.js} +1 -1
- package/dist/{chunk-bh6k3nc6.js → chunk-4p9rh7jj.js} +1 -1
- package/dist/{chunk-34bqh420.js → chunk-4z3n1dzt.js} +59 -21
- package/dist/{chunk-34bqh420.js.map → chunk-4z3n1dzt.js.map} +4 -4
- package/dist/chunk-96swp2wd.js +47 -0
- package/dist/chunk-96swp2wd.js.map +10 -0
- package/dist/{chunk-n8herkry.js → chunk-bv41fmjn.js} +11 -11
- package/dist/{chunk-7pc8b818.js → chunk-c6cfqydk.js} +1 -1
- package/dist/{chunk-0k4avqr2.js → chunk-c8bw2b6c.js} +2 -2
- package/dist/{chunk-jaj13078.js → chunk-d2adj15j.js} +3 -33
- package/dist/{chunk-jaj13078.js.map → chunk-d2adj15j.js.map} +3 -4
- package/dist/{chunk-c6qd89g5.js → chunk-d6y2wsfg.js} +3 -3
- package/dist/chunk-dysw7nsm.js.map +1 -1
- package/dist/{chunk-st4xgxey.js → chunk-ettb0cy4.js} +150 -23
- package/dist/chunk-ettb0cy4.js.map +45 -0
- package/dist/{chunk-nnyp53zt.js → chunk-f1cyvnrk.js} +1 -1
- package/dist/chunk-fmx1sz2h.js.map +2 -2
- package/dist/{chunk-bct7hmfc.js → chunk-km4rkj4t.js} +3 -3
- package/dist/chunk-kqxgf6kv.js +898 -0
- package/dist/chunk-kqxgf6kv.js.map +15 -0
- package/dist/{chunk-qvybnnpe.js → chunk-n5x5y80a.js} +2 -2
- package/dist/{chunk-4x43bkbp.js → chunk-pxtrxm3p.js} +1 -1
- package/dist/chunk-qer6am8w.js +133 -0
- package/dist/chunk-qer6am8w.js.map +11 -0
- package/dist/{chunk-85j5ybfj.js → chunk-rdeagz6b.js} +2 -2
- package/dist/{chunk-85j5ybfj.js.map → chunk-rdeagz6b.js.map} +3 -3
- package/dist/chunk-rhrbz7d4.js +36 -0
- package/dist/chunk-rhrbz7d4.js.map +10 -0
- package/dist/{chunk-663csy96.js → chunk-t0jjkep3.js} +2 -1
- package/dist/chunk-t0jjkep3.js.map +10 -0
- package/dist/{chunk-5egfka8j.js → chunk-ttba8x6z.js} +2 -2
- package/dist/{chunk-nten49ta.js → chunk-wb4051cx.js} +3 -3
- package/dist/{chunk-nten49ta.js.map → chunk-wb4051cx.js.map} +1 -1
- package/dist/{chunk-57r1cpxx.js → chunk-xc7hq65b.js} +3 -2
- package/dist/{chunk-57r1cpxx.js.map → chunk-xc7hq65b.js.map} +3 -3
- package/dist/{chunk-ys1nse2v.js → chunk-xmggaxg4.js} +256 -231
- package/dist/{chunk-ys1nse2v.js.map → chunk-xmggaxg4.js.map} +8 -10
- package/dist/{chunk-g9t3kdtx.js → chunk-z8j83gzd.js} +1 -1
- package/dist/{chunk-57zpb99z.js → chunk-zpw39qhj.js} +1 -1
- package/dist/execution/agent.js +23 -20
- package/dist/execution/agent.js.map +1 -1
- package/dist/execution/primitive.js +23 -20
- package/dist/execution/primitive.js.map +2 -2
- package/dist/execution/request.js +24 -21
- package/dist/execution/request.js.map +2 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +32 -29
- package/dist/index.js.map +1 -1
- package/dist/materialization/model.d.ts +4 -0
- package/dist/materialization/model.d.ts.map +1 -1
- package/dist/materializer/edits/apply.d.ts +6 -1
- package/dist/materializer/edits/apply.d.ts.map +1 -1
- package/dist/materializer/edits/working-state.d.ts +2 -0
- package/dist/materializer/edits/working-state.d.ts.map +1 -1
- package/dist/materializer/effective/resolve.d.ts +3 -1
- package/dist/materializer/effective/resolve.d.ts.map +1 -1
- package/dist/materializer/execution/work-records.d.ts.map +1 -1
- package/dist/materializer/index.js +14 -11
- package/dist/materializer/index.js.map +1 -1
- package/dist/materializer/projections/replacement-plan.d.ts.map +1 -1
- package/dist/materializer/shared/normalize.d.ts.map +1 -1
- package/dist/objects/index.js +17 -14
- package/dist/objects/index.js.map +1 -1
- package/dist/objects/query/index.js +2 -2
- package/dist/pipelines/index.js +14 -12
- package/dist/pipelines/index.js.map +1 -1
- package/dist/projections/builders.d.ts +19 -6
- package/dist/projections/builders.d.ts.map +1 -1
- package/dist/projections/index.d.ts +2 -1
- package/dist/projections/index.d.ts.map +1 -1
- package/dist/projections/internal.d.ts +4 -1
- package/dist/projections/internal.d.ts.map +1 -1
- package/dist/projections/internal.js +17 -11
- package/dist/projections/internal.js.map +1 -1
- package/dist/projections/timestamp.d.ts +3 -0
- package/dist/projections/timestamp.d.ts.map +1 -0
- package/dist/projections/types.d.ts +9 -0
- package/dist/projections/types.d.ts.map +1 -1
- package/dist/projections/validation.d.ts.map +1 -1
- package/dist/queues/index.d.ts +1 -1
- package/dist/queues/index.d.ts.map +1 -1
- package/dist/queues/types.d.ts +7 -0
- package/dist/queues/types.d.ts.map +1 -1
- package/dist/runtime/ai-cost.d.ts +8 -0
- package/dist/runtime/ai-cost.d.ts.map +1 -0
- package/dist/storage/agents/in-memory.d.ts +18 -4
- package/dist/storage/agents/in-memory.d.ts.map +1 -1
- package/dist/storage/agents/index.d.ts +1 -1
- package/dist/storage/agents/index.d.ts.map +1 -1
- package/dist/storage/agents/provider.d.ts +31 -0
- package/dist/storage/agents/provider.d.ts.map +1 -1
- package/dist/storage/agents/provider.js +18 -7
- package/dist/storage/agents/provider.js.map +1 -1
- package/dist/storage/agents/types.d.ts +70 -4
- package/dist/storage/agents/types.d.ts.map +1 -1
- package/dist/storage/ai-cost/analytics.d.ts +61 -0
- package/dist/storage/ai-cost/analytics.d.ts.map +1 -0
- package/dist/storage/ai-cost/codec.d.ts +14 -0
- package/dist/storage/ai-cost/codec.d.ts.map +1 -0
- package/dist/storage/ai-cost/errors.d.ts +7 -0
- package/dist/storage/ai-cost/errors.d.ts.map +1 -0
- package/dist/storage/ai-cost/in-memory.d.ts +32 -0
- package/dist/storage/ai-cost/in-memory.d.ts.map +1 -0
- package/dist/storage/ai-cost/index.d.ts +6 -0
- package/dist/storage/ai-cost/index.d.ts.map +1 -0
- package/dist/storage/ai-cost/provider.d.ts +7 -0
- package/dist/storage/ai-cost/provider.d.ts.map +1 -0
- package/dist/storage/ai-cost/provider.js +132 -0
- package/dist/storage/ai-cost/provider.js.map +10 -0
- package/dist/storage/ai-cost/types.d.ts +154 -0
- package/dist/storage/ai-cost/types.d.ts.map +1 -0
- package/dist/storage/ai-cost/validation.d.ts +7 -0
- package/dist/storage/ai-cost/validation.d.ts.map +1 -0
- package/dist/storage/in-memory/index.d.ts +5 -0
- package/dist/storage/in-memory/index.d.ts.map +1 -1
- package/dist/storage/index.d.ts +3 -1
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +22 -15
- package/dist/storage/index.js.map +1 -1
- package/dist/storage/ontology/in-memory/materializations.d.ts.map +1 -1
- package/dist/storage/ontology/materializations.d.ts +3 -0
- package/dist/storage/ontology/materializations.d.ts.map +1 -1
- package/dist/storage/operation-scope.d.ts.map +1 -1
- package/dist/storage/operation-scope.js +1 -1
- package/dist/storage/types.d.ts +2 -0
- package/dist/storage/types.d.ts.map +1 -1
- package/dist/storage/workflow-runs/provider.js +6 -5
- package/dist/storage/workflow-runs/provider.js.map +1 -1
- package/dist/syncs/index.js +14 -12
- package/dist/syncs/index.js.map +1 -1
- package/dist/testing/agent-storage-contract.d.ts +3 -2
- package/dist/testing/agent-storage-contract.d.ts.map +1 -1
- package/dist/testing/ai-cost-storage-contract.d.ts +12 -0
- package/dist/testing/ai-cost-storage-contract.d.ts.map +1 -0
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +2098 -1468
- package/dist/testing/index.js.map +6 -5
- package/dist/testing/materializer-storage-contract.d.ts.map +1 -1
- package/dist/webhooks/internal.js +12 -10
- package/dist/webhooks/internal.js.map +2 -2
- package/dist/workers/index.js +12 -2
- package/dist/workers/index.js.map +3 -3
- package/dist/workers/queue-worker.d.ts +2 -0
- package/dist/workers/queue-worker.d.ts.map +1 -1
- package/dist/workflows/execution.d.ts +3 -0
- package/dist/workflows/execution.d.ts.map +1 -1
- package/dist/workflows/index.js +24 -21
- package/dist/workflows/index.js.map +1 -1
- package/package.json +7 -1
- package/src/agents/adapters.ts +64 -23
- package/src/agents/errors.ts +2 -2
- package/src/agents/execution.ts +14 -2
- package/src/agents/index.ts +18 -1
- package/src/agents/prompt.ts +33 -5
- package/src/agents/request.ts +5 -0
- package/src/agents/thread-context-projection.ts +93 -0
- package/src/agents/tool-definition.ts +17 -2
- package/src/agents/tool-result.ts +61 -0
- package/src/agents/types.ts +47 -1
- package/src/agents/validation.ts +1 -1
- package/src/index.ts +13 -0
- package/src/materialization/model.ts +4 -0
- package/src/materializer/edits/apply.ts +55 -2
- package/src/materializer/edits/operations.ts +8 -1
- package/src/materializer/edits/working-state.ts +7 -0
- package/src/materializer/effective/resolve.ts +78 -9
- package/src/materializer/execution/work-records.ts +3 -1
- package/src/materializer/projections/entry-validator.ts +23 -1
- package/src/materializer/projections/replacement-plan.ts +2 -0
- package/src/materializer/shared/normalize.ts +16 -0
- package/src/projections/builders.ts +100 -35
- package/src/projections/index.ts +6 -0
- package/src/projections/internal.ts +4 -1
- package/src/projections/registry.ts +7 -0
- package/src/projections/revision.ts +4 -0
- package/src/projections/timestamp.ts +66 -0
- package/src/projections/types.ts +7 -0
- package/src/projections/validation.ts +24 -0
- package/src/queues/index.ts +1 -0
- package/src/queues/types.ts +8 -0
- package/src/runtime/ai-cost.ts +15 -0
- package/src/storage/agents/in-memory.ts +171 -4
- package/src/storage/agents/index.ts +5 -0
- package/src/storage/agents/provider.ts +225 -0
- package/src/storage/agents/types.ts +77 -3
- package/src/storage/ai-cost/analytics.ts +602 -0
- package/src/storage/ai-cost/codec.ts +136 -0
- package/src/storage/ai-cost/errors.ts +12 -0
- package/src/storage/ai-cost/in-memory.ts +164 -0
- package/src/storage/ai-cost/index.ts +35 -0
- package/src/storage/ai-cost/provider.ts +20 -0
- package/src/storage/ai-cost/types.ts +193 -0
- package/src/storage/ai-cost/validation.ts +337 -0
- package/src/storage/in-memory/index.ts +50 -0
- package/src/storage/index.ts +37 -0
- package/src/storage/ontology/in-memory/materializations.ts +1 -0
- package/src/storage/ontology/materializations.ts +3 -0
- package/src/storage/operation-scope.ts +1 -0
- package/src/storage/types.ts +2 -0
- package/src/testing/agent-storage-contract.ts +341 -2
- package/src/testing/ai-cost-storage-contract.ts +320 -0
- package/src/testing/index.ts +5 -0
- package/src/testing/materializer-storage-contract.ts +120 -2
- package/src/workers/queue-worker.ts +14 -1
- package/src/workflows/execution.ts +31 -10
- package/dist/chunk-0mbx247x.js.map +0 -43
- package/dist/chunk-663csy96.js.map +0 -10
- package/dist/chunk-st4xgxey.js.map +0 -45
- package/dist/chunk-vp1vc6ee.js +0 -35
- package/dist/chunk-vp1vc6ee.js.map +0 -11
- /package/dist/{chunk-sa4340y1.js.map → chunk-1ywq4hzk.js.map} +0 -0
- /package/dist/{chunk-zg9yryhz.js.map → chunk-261ag4t4.js.map} +0 -0
- /package/dist/{chunk-k9pts48t.js.map → chunk-3rsf30p4.js.map} +0 -0
- /package/dist/{chunk-8ryen3p3.js.map → chunk-4aw0jwk0.js.map} +0 -0
- /package/dist/{chunk-bh6k3nc6.js.map → chunk-4p9rh7jj.js.map} +0 -0
- /package/dist/{chunk-n8herkry.js.map → chunk-bv41fmjn.js.map} +0 -0
- /package/dist/{chunk-7pc8b818.js.map → chunk-c6cfqydk.js.map} +0 -0
- /package/dist/{chunk-0k4avqr2.js.map → chunk-c8bw2b6c.js.map} +0 -0
- /package/dist/{chunk-c6qd89g5.js.map → chunk-d6y2wsfg.js.map} +0 -0
- /package/dist/{chunk-nnyp53zt.js.map → chunk-f1cyvnrk.js.map} +0 -0
- /package/dist/{chunk-bct7hmfc.js.map → chunk-km4rkj4t.js.map} +0 -0
- /package/dist/{chunk-qvybnnpe.js.map → chunk-n5x5y80a.js.map} +0 -0
- /package/dist/{chunk-4x43bkbp.js.map → chunk-pxtrxm3p.js.map} +0 -0
- /package/dist/{chunk-5egfka8j.js.map → chunk-ttba8x6z.js.map} +0 -0
- /package/dist/{chunk-g9t3kdtx.js.map → chunk-z8j83gzd.js.map} +0 -0
- /package/dist/{chunk-57zpb99z.js.map → chunk-zpw39qhj.js.map} +0 -0
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
resolveEffectiveLink,
|
|
26
26
|
resolveEffectiveLinkSlot,
|
|
27
27
|
resolveEffectiveObject,
|
|
28
|
+
storedObjectEditedAt,
|
|
28
29
|
usableLinkSlotOverride,
|
|
29
30
|
} from "../effective/resolve"
|
|
30
31
|
import { linkCardinality } from "./read-set"
|
|
@@ -33,6 +34,8 @@ export interface WorkingObject {
|
|
|
33
34
|
source: MaterializationObjectState["source"]
|
|
34
35
|
originalOverride: StoredObjectOverride | null
|
|
35
36
|
override: ObjectOverride | null
|
|
37
|
+
originalEditedAt: Readonly<Record<string, string>>
|
|
38
|
+
editedAt: Record<string, string>
|
|
36
39
|
before: MaterializationObjectState["effective"]
|
|
37
40
|
latestTelemetry: StoredTelemetryPoint[]
|
|
38
41
|
}
|
|
@@ -112,11 +115,14 @@ function workingLinkSlotFromState(scope: MaterializationLinkScopeState): Working
|
|
|
112
115
|
}
|
|
113
116
|
|
|
114
117
|
export function workingObjectFromState(object: MaterializationObjectState): WorkingObject {
|
|
118
|
+
const editedAt = storedObjectEditedAt(object.override)
|
|
115
119
|
return {
|
|
116
120
|
ref: object.ref,
|
|
117
121
|
source: object.source,
|
|
118
122
|
originalOverride: object.override,
|
|
119
123
|
override: object.override?.value ?? null,
|
|
124
|
+
originalEditedAt: editedAt,
|
|
125
|
+
editedAt: { ...editedAt },
|
|
120
126
|
before: object.effective,
|
|
121
127
|
latestTelemetry: [...object.latestTelemetry],
|
|
122
128
|
}
|
|
@@ -141,6 +147,7 @@ export function resolveObject(
|
|
|
141
147
|
primaryPropertyId: ontology.getPrimaryPropertyId(working.ref.objectTypeId),
|
|
142
148
|
source: working.source,
|
|
143
149
|
override: working.override,
|
|
150
|
+
editedAt: working.editedAt,
|
|
144
151
|
latestTelemetry: working.latestTelemetry,
|
|
145
152
|
})
|
|
146
153
|
}
|
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
import { linkRefKey } from "../../materialization/refs"
|
|
12
12
|
import type {
|
|
13
13
|
StoredLinkSlotOverride,
|
|
14
|
+
StoredObjectOverride,
|
|
14
15
|
StoredSourceLinkAssertion,
|
|
15
16
|
StoredSourceObjectAssertion,
|
|
16
17
|
StoredTelemetryPoint,
|
|
@@ -46,9 +47,10 @@ export function resolveEffectiveObject(input: {
|
|
|
46
47
|
readonly primaryPropertyId: string
|
|
47
48
|
readonly source: StoredSourceObjectAssertion | null
|
|
48
49
|
readonly override: ObjectOverride | null
|
|
50
|
+
readonly editedAt: Readonly<Record<string, string>>
|
|
49
51
|
readonly latestTelemetry: readonly StoredTelemetryPoint[]
|
|
50
52
|
}): ResolvedObjectValue | null {
|
|
51
|
-
const properties = resolveObjectAuthority(input.source, input.override)
|
|
53
|
+
const properties = resolveObjectAuthority(input.source, input.override, input.editedAt)
|
|
52
54
|
if (!properties) return null
|
|
53
55
|
|
|
54
56
|
properties[input.primaryPropertyId] = input.ref.primaryId
|
|
@@ -58,31 +60,98 @@ export function resolveEffectiveObject(input: {
|
|
|
58
60
|
|
|
59
61
|
function resolveObjectAuthority(
|
|
60
62
|
source: StoredSourceObjectAssertion | null,
|
|
61
|
-
override: ObjectOverride | null
|
|
63
|
+
override: ObjectOverride | null,
|
|
64
|
+
editedAt: Readonly<Record<string, string>>
|
|
62
65
|
): Record<string, JsonValue> | null {
|
|
63
66
|
switch (override?.kind) {
|
|
64
67
|
case "delete":
|
|
65
68
|
return null
|
|
66
69
|
case "create":
|
|
67
|
-
return { ...override.properties }
|
|
70
|
+
if (!source) return { ...override.properties }
|
|
71
|
+
return resolveSourceAndEdits(source, override, editedAt)
|
|
68
72
|
case "patch":
|
|
69
73
|
if (!source) return null
|
|
70
|
-
return
|
|
74
|
+
return resolveSourceAndEdits(source, override, editedAt)
|
|
71
75
|
default:
|
|
72
76
|
if (!source) return null
|
|
73
77
|
return { ...source.assertion.properties }
|
|
74
78
|
}
|
|
75
79
|
}
|
|
76
80
|
|
|
77
|
-
function
|
|
78
|
-
|
|
79
|
-
override:
|
|
81
|
+
function resolveSourceAndEdits(
|
|
82
|
+
source: StoredSourceObjectAssertion,
|
|
83
|
+
override: Exclude<ObjectOverride, { readonly kind: "delete" }>,
|
|
84
|
+
editedAt: Readonly<Record<string, string>>
|
|
80
85
|
): Record<string, JsonValue> {
|
|
81
|
-
const properties = { ...
|
|
82
|
-
|
|
86
|
+
const properties = { ...source.assertion.properties }
|
|
87
|
+
const absentSourcePropertyIds = new Set(source.assertion.absentSourcePropertyIds ?? [])
|
|
88
|
+
|
|
89
|
+
for (const propertyId of objectEditCandidateIds(override, editedAt)) {
|
|
90
|
+
const edit = objectEditCandidate(override, propertyId)
|
|
91
|
+
const editTimestamp = editedAt[propertyId]
|
|
92
|
+
const hasSourceCandidate =
|
|
93
|
+
Object.hasOwn(source.assertion.properties, propertyId) ||
|
|
94
|
+
absentSourcePropertyIds.has(propertyId)
|
|
95
|
+
const sourceWins =
|
|
96
|
+
hasSourceCandidate &&
|
|
97
|
+
source.assertion.sourceUpdatedAt !== undefined &&
|
|
98
|
+
editTimestamp !== undefined &&
|
|
99
|
+
source.assertion.sourceUpdatedAt.localeCompare(editTimestamp) >= 0
|
|
100
|
+
|
|
101
|
+
if (!sourceWins) applyObjectEditCandidate(properties, propertyId, edit)
|
|
102
|
+
}
|
|
83
103
|
return properties
|
|
84
104
|
}
|
|
85
105
|
|
|
106
|
+
interface ObjectPropertyCandidate {
|
|
107
|
+
readonly present: boolean
|
|
108
|
+
readonly value?: JsonValue
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function objectEditCandidateIds(
|
|
112
|
+
override: Exclude<ObjectOverride, { readonly kind: "delete" }>,
|
|
113
|
+
editedAt: Readonly<Record<string, string>>
|
|
114
|
+
): string[] {
|
|
115
|
+
const ids = new Set(Object.keys(editedAt))
|
|
116
|
+
if (override.kind === "create") {
|
|
117
|
+
for (const propertyId of Object.keys(override.properties)) ids.add(propertyId)
|
|
118
|
+
} else {
|
|
119
|
+
for (const propertyId of Object.keys(override.set)) ids.add(propertyId)
|
|
120
|
+
for (const propertyId of override.unset) ids.add(propertyId)
|
|
121
|
+
}
|
|
122
|
+
return [...ids]
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function objectEditCandidate(
|
|
126
|
+
override: Exclude<ObjectOverride, { readonly kind: "delete" }>,
|
|
127
|
+
propertyId: string
|
|
128
|
+
): ObjectPropertyCandidate {
|
|
129
|
+
const values = override.kind === "create" ? override.properties : override.set
|
|
130
|
+
return Object.hasOwn(values, propertyId)
|
|
131
|
+
? { present: true, value: values[propertyId] }
|
|
132
|
+
: { present: false }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function applyObjectEditCandidate(
|
|
136
|
+
properties: Record<string, JsonValue>,
|
|
137
|
+
propertyId: string,
|
|
138
|
+
candidate: ObjectPropertyCandidate
|
|
139
|
+
): void {
|
|
140
|
+
if (candidate.present) properties[propertyId] = candidate.value as JsonValue
|
|
141
|
+
else delete properties[propertyId]
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function storedObjectEditedAt(stored: StoredObjectOverride | null): Record<string, string> {
|
|
145
|
+
if (!stored || stored.value.kind === "delete") return {}
|
|
146
|
+
const editedAt = { ...(stored.editedAt ?? {}) }
|
|
147
|
+
const candidateIds =
|
|
148
|
+
stored.value.kind === "create"
|
|
149
|
+
? Object.keys(stored.value.properties)
|
|
150
|
+
: [...Object.keys(stored.value.set), ...stored.value.unset]
|
|
151
|
+
for (const propertyId of candidateIds) editedAt[propertyId] ??= stored.updatedAt
|
|
152
|
+
return editedAt
|
|
153
|
+
}
|
|
154
|
+
|
|
86
155
|
function applyLatestTelemetry(
|
|
87
156
|
properties: Record<string, JsonValue>,
|
|
88
157
|
latestTelemetry: readonly StoredTelemetryPoint[]
|
|
@@ -22,7 +22,8 @@ export function appendObjectOverrideWork(
|
|
|
22
22
|
): void {
|
|
23
23
|
if (
|
|
24
24
|
stableJsonStringify(working.originalOverride?.value ?? null) ===
|
|
25
|
-
|
|
25
|
+
stableJsonStringify(working.override) &&
|
|
26
|
+
stableJsonStringify(working.originalEditedAt) === stableJsonStringify(working.editedAt)
|
|
26
27
|
) {
|
|
27
28
|
return
|
|
28
29
|
}
|
|
@@ -32,6 +33,7 @@ export function appendObjectOverrideWork(
|
|
|
32
33
|
value: {
|
|
33
34
|
ref: working.ref,
|
|
34
35
|
value: working.override,
|
|
36
|
+
editedAt: working.editedAt,
|
|
35
37
|
expectedLastCommitId: working.originalOverride?.lastCommitId ?? null,
|
|
36
38
|
lastCommitId: identity.commitId,
|
|
37
39
|
updatedAt: identity.committedAt,
|
|
@@ -118,7 +118,29 @@ function validateObjectAssertion(
|
|
|
118
118
|
`Projection '${resolved.projectionId}' asserted unowned property '${assertion.ref.objectTypeId}.${propertyId}'.`
|
|
119
119
|
)
|
|
120
120
|
}
|
|
121
|
-
|
|
121
|
+
const conflictResolution = resolved.definition.conflictResolution ?? { strategy: "editsWin" }
|
|
122
|
+
if (conflictResolution.strategy !== "mostRecent") {
|
|
123
|
+
return { kind: "object", ref: assertion.ref, properties }
|
|
124
|
+
}
|
|
125
|
+
if (assertion.sourceUpdatedAt === undefined) {
|
|
126
|
+
throw new MaterializationValidationError(
|
|
127
|
+
`Projection '${resolved.projectionId}' requires source update timestamp ` +
|
|
128
|
+
`'${conflictResolution.sourceTimestamp}' on every object assertion.`
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
const primaryPropertyId = ontology.getPrimaryPropertyId(assertion.ref.objectTypeId)
|
|
132
|
+
const absentSourcePropertyIds = [...owned]
|
|
133
|
+
.filter(
|
|
134
|
+
(propertyId) => propertyId !== primaryPropertyId && !Object.hasOwn(properties, propertyId)
|
|
135
|
+
)
|
|
136
|
+
.sort()
|
|
137
|
+
return {
|
|
138
|
+
kind: "object",
|
|
139
|
+
ref: assertion.ref,
|
|
140
|
+
properties,
|
|
141
|
+
sourceUpdatedAt: assertion.sourceUpdatedAt,
|
|
142
|
+
...(absentSourcePropertyIds.length === 0 ? {} : { absentSourcePropertyIds }),
|
|
143
|
+
}
|
|
122
144
|
}
|
|
123
145
|
|
|
124
146
|
function validateLinkAssertion(
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
resolveEffectiveLink,
|
|
34
34
|
resolveEffectiveLinkSlotMember,
|
|
35
35
|
resolveEffectiveObject,
|
|
36
|
+
storedObjectEditedAt,
|
|
36
37
|
usableLinkSlotOverride,
|
|
37
38
|
} from "../effective/resolve"
|
|
38
39
|
import { validateEffectiveObject } from "../effective/validate"
|
|
@@ -374,6 +375,7 @@ function resolveReplacementObject(
|
|
|
374
375
|
primaryPropertyId: context.ontology.getPrimaryPropertyId(state.ref.objectTypeId),
|
|
375
376
|
source: state.candidateSource,
|
|
376
377
|
override: state.override?.value ?? null,
|
|
378
|
+
editedAt: storedObjectEditedAt(state.override),
|
|
377
379
|
latestTelemetry: state.latestTelemetry,
|
|
378
380
|
})
|
|
379
381
|
}
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
projectionEntityKey,
|
|
26
26
|
telemetryPointKey,
|
|
27
27
|
} from "../../materialization/refs"
|
|
28
|
+
import { parseDatasetTimestamp } from "../../projections/timestamp"
|
|
28
29
|
|
|
29
30
|
export function normalizeJsonProperties(
|
|
30
31
|
properties: Readonly<Record<string, unknown>>,
|
|
@@ -67,6 +68,16 @@ function normalizeTimestamp(value: string, label: string): string {
|
|
|
67
68
|
return new Date(milliseconds).toISOString()
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
function normalizeProjectionSourceTimestamp(value: string): string {
|
|
72
|
+
const timestamp = parseDatasetTimestamp(value)
|
|
73
|
+
if (!timestamp) {
|
|
74
|
+
throw new MaterializationValidationError(
|
|
75
|
+
"Projection source update timestamp must be a valid timestamp."
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
return timestamp.toISOString()
|
|
79
|
+
}
|
|
80
|
+
|
|
70
81
|
function normalizePropertyIds(values: readonly string[], label: string): readonly string[] {
|
|
71
82
|
const seen = new Set<string>()
|
|
72
83
|
const normalized: string[] = []
|
|
@@ -347,6 +358,11 @@ function normalizeProjectionAssertion(
|
|
|
347
358
|
kind: "object",
|
|
348
359
|
ref: normalizeObjectRef(assertion.ref),
|
|
349
360
|
properties: normalizeJsonProperties(assertion.properties),
|
|
361
|
+
...(assertion.sourceUpdatedAt !== undefined
|
|
362
|
+
? {
|
|
363
|
+
sourceUpdatedAt: normalizeProjectionSourceTimestamp(assertion.sourceUpdatedAt),
|
|
364
|
+
}
|
|
365
|
+
: {}),
|
|
350
366
|
})
|
|
351
367
|
: Object.freeze({
|
|
352
368
|
kind: "link",
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type {
|
|
8
|
+
DatasetColumnDefinitionOf,
|
|
8
9
|
DatasetColumnNameOf,
|
|
9
10
|
DatasetColumnType,
|
|
10
11
|
DatasetColumnTypeOf,
|
|
@@ -21,6 +22,7 @@ import type {
|
|
|
21
22
|
ObjectProjectionTarget,
|
|
22
23
|
ProjectionDefinition,
|
|
23
24
|
ProjectionTarget,
|
|
25
|
+
SourceEditConflictResolution,
|
|
24
26
|
TelemetryProjectionDefinition,
|
|
25
27
|
} from "./types"
|
|
26
28
|
import {
|
|
@@ -153,6 +155,20 @@ type DateLikeDatasetColumnNameOf<TDataset extends DatasetDefinition> =
|
|
|
153
155
|
: never
|
|
154
156
|
}[DatasetColumnNameOf<TDataset>]
|
|
155
157
|
|
|
158
|
+
type TimestampDatasetColumnNameOf<TDataset extends DatasetDefinition> =
|
|
159
|
+
string extends DatasetColumnNameOf<TDataset>
|
|
160
|
+
? string
|
|
161
|
+
: {
|
|
162
|
+
[TColumnName in DatasetColumnNameOf<TDataset>]: DatasetColumnTypeOf<
|
|
163
|
+
TDataset,
|
|
164
|
+
TColumnName
|
|
165
|
+
> extends "timestamp"
|
|
166
|
+
? DatasetColumnDefinitionOf<TDataset, TColumnName> extends { readonly nullable: true }
|
|
167
|
+
? never
|
|
168
|
+
: TColumnName
|
|
169
|
+
: never
|
|
170
|
+
}[DatasetColumnNameOf<TDataset>]
|
|
171
|
+
|
|
156
172
|
type ProjectionMappingFor<TObjectType extends ObjectType, TDataset extends DatasetDefinition> = {
|
|
157
173
|
readonly [TPropertyId in PropertyIdOf<TObjectType>]?: DatasetColumnNameCompatibleWithSchema<
|
|
158
174
|
TDataset,
|
|
@@ -239,7 +255,7 @@ type ProjectionForeignKeyTokenInput<
|
|
|
239
255
|
)
|
|
240
256
|
}
|
|
241
257
|
|
|
242
|
-
type ProjectionForeignKeyInput<
|
|
258
|
+
export type ProjectionForeignKeyInput<
|
|
243
259
|
TObjectType extends ObjectType,
|
|
244
260
|
TDataset extends DatasetDefinition,
|
|
245
261
|
TLinkId extends LinkIdOf<TObjectType>,
|
|
@@ -280,16 +296,27 @@ interface ObjectProjectionMappingBuilder<
|
|
|
280
296
|
> {
|
|
281
297
|
properties<const TMapping extends ProjectionMappingFor<TObjectType, TDataset>>(
|
|
282
298
|
mapping: ExactProjectionMapping<TObjectType, TMapping>
|
|
283
|
-
):
|
|
299
|
+
): ObjectProjectionBuilder<TObjectType, TDataset>
|
|
284
300
|
}
|
|
285
301
|
|
|
286
|
-
|
|
302
|
+
export type ObjectProjectionConflictResolution<TDataset extends DatasetDefinition> =
|
|
303
|
+
| { readonly strategy: "editsWin" }
|
|
304
|
+
| {
|
|
305
|
+
readonly strategy: "mostRecent"
|
|
306
|
+
readonly sourceTimestamp: TimestampDatasetColumnNameOf<TDataset>
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export interface ObjectProjectionBuilder<
|
|
287
310
|
TObjectType extends ObjectType,
|
|
288
311
|
TDataset extends DatasetDefinition,
|
|
289
|
-
> {
|
|
312
|
+
> extends ObjectProjectionDefinition {
|
|
313
|
+
readonly conflictResolution: SourceEditConflictResolution
|
|
290
314
|
withLinks(
|
|
291
315
|
mapping: { [K in LinkIdOf<TObjectType>]?: ProjectionForeignKeyInput<TObjectType, TDataset, K> }
|
|
292
|
-
):
|
|
316
|
+
): ObjectProjectionBuilder<TObjectType, TDataset>
|
|
317
|
+
resolveConflicts(
|
|
318
|
+
resolution: ObjectProjectionConflictResolution<TDataset>
|
|
319
|
+
): ObjectProjectionBuilder<TObjectType, TDataset>
|
|
293
320
|
}
|
|
294
321
|
|
|
295
322
|
/**
|
|
@@ -352,41 +379,17 @@ function buildObjectProjection<const TObjectType extends ObjectType>(
|
|
|
352
379
|
return {
|
|
353
380
|
properties<const TMapping extends ProjectionMappingFor<TObjectType, TDataset>>(
|
|
354
381
|
mapping: ExactProjectionMapping<TObjectType, TMapping>
|
|
355
|
-
):
|
|
382
|
+
): ObjectProjectionBuilder<TObjectType, TDataset> {
|
|
356
383
|
const propertyMapping = mapping as Record<string, string>
|
|
357
384
|
validatePropertyMapping(objectType, propertyMapping)
|
|
358
385
|
|
|
359
|
-
|
|
360
|
-
_tag: "ObjectProjectionDefinition",
|
|
386
|
+
return objectProjectionBuilder({
|
|
361
387
|
id,
|
|
362
|
-
|
|
388
|
+
objectType,
|
|
363
389
|
datasetId,
|
|
364
|
-
|
|
390
|
+
propertyMapping,
|
|
365
391
|
links: {},
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
return Object.assign(definition, {
|
|
369
|
-
withLinks(
|
|
370
|
-
linkMapping: {
|
|
371
|
-
[K in LinkIdOf<TObjectType>]?: ProjectionForeignKeyInput<TObjectType, TDataset, K>
|
|
372
|
-
}
|
|
373
|
-
): ObjectProjectionDefinition {
|
|
374
|
-
const loweredLinkMapping = lowerForeignKeyMapping(linkMapping)
|
|
375
|
-
const validatedLinks = validateAndLowerLinkMapping(
|
|
376
|
-
objectType,
|
|
377
|
-
loweredLinkMapping,
|
|
378
|
-
propertyMapping
|
|
379
|
-
)
|
|
380
|
-
|
|
381
|
-
return {
|
|
382
|
-
_tag: "ObjectProjectionDefinition",
|
|
383
|
-
id,
|
|
384
|
-
objectTypeId: objectType.id,
|
|
385
|
-
datasetId,
|
|
386
|
-
properties: { ...propertyMapping },
|
|
387
|
-
links: validatedLinks,
|
|
388
|
-
}
|
|
389
|
-
},
|
|
392
|
+
conflictResolution: { strategy: "editsWin" },
|
|
390
393
|
})
|
|
391
394
|
},
|
|
392
395
|
}
|
|
@@ -394,6 +397,60 @@ function buildObjectProjection<const TObjectType extends ObjectType>(
|
|
|
394
397
|
}
|
|
395
398
|
}
|
|
396
399
|
|
|
400
|
+
function objectProjectionBuilder<
|
|
401
|
+
TObjectType extends ObjectType,
|
|
402
|
+
TDataset extends DatasetDefinition,
|
|
403
|
+
>(input: {
|
|
404
|
+
readonly id: string
|
|
405
|
+
readonly objectType: TObjectType
|
|
406
|
+
readonly datasetId: string
|
|
407
|
+
readonly propertyMapping: Readonly<Record<string, string>>
|
|
408
|
+
readonly links: Readonly<Record<string, ForeignKeyDescriptor>>
|
|
409
|
+
readonly conflictResolution: SourceEditConflictResolution
|
|
410
|
+
}): ObjectProjectionBuilder<TObjectType, TDataset> {
|
|
411
|
+
const definition: ObjectProjectionDefinition & {
|
|
412
|
+
readonly conflictResolution: SourceEditConflictResolution
|
|
413
|
+
} = {
|
|
414
|
+
_tag: "ObjectProjectionDefinition",
|
|
415
|
+
id: input.id,
|
|
416
|
+
objectTypeId: input.objectType.id,
|
|
417
|
+
datasetId: input.datasetId,
|
|
418
|
+
properties: { ...input.propertyMapping },
|
|
419
|
+
links: { ...input.links },
|
|
420
|
+
conflictResolution: { ...input.conflictResolution },
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return Object.assign(definition, {
|
|
424
|
+
withLinks(
|
|
425
|
+
linkMapping: {
|
|
426
|
+
[K in LinkIdOf<TObjectType>]?: ProjectionForeignKeyInput<TObjectType, TDataset, K>
|
|
427
|
+
}
|
|
428
|
+
): ObjectProjectionBuilder<TObjectType, TDataset> {
|
|
429
|
+
const loweredLinkMapping = lowerForeignKeyMapping(linkMapping)
|
|
430
|
+
const validatedLinks = validateAndLowerLinkMapping(
|
|
431
|
+
input.objectType,
|
|
432
|
+
loweredLinkMapping,
|
|
433
|
+
input.propertyMapping as Record<string, string>
|
|
434
|
+
)
|
|
435
|
+
return objectProjectionBuilder({ ...input, links: validatedLinks })
|
|
436
|
+
},
|
|
437
|
+
resolveConflicts(
|
|
438
|
+
resolution: ObjectProjectionConflictResolution<TDataset>
|
|
439
|
+
): ObjectProjectionBuilder<TObjectType, TDataset> {
|
|
440
|
+
validateConflictResolution(resolution)
|
|
441
|
+
return objectProjectionBuilder({
|
|
442
|
+
...input,
|
|
443
|
+
conflictResolution: resolution as SourceEditConflictResolution,
|
|
444
|
+
})
|
|
445
|
+
},
|
|
446
|
+
})
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function validateConflictResolution(resolution: SourceEditConflictResolution): void {
|
|
450
|
+
if (resolution.strategy === "editsWin") return
|
|
451
|
+
assertNonEmpty(resolution.sourceTimestamp, "source timestamp field")
|
|
452
|
+
}
|
|
453
|
+
|
|
397
454
|
// ── Telemetry projection ────────────────────────────────────
|
|
398
455
|
|
|
399
456
|
interface TelemetryProjectionSourceBuilder<TPropertyToken extends TelemetryPropertyToken> {
|
|
@@ -672,10 +729,18 @@ export function isObjectProjectionDefinition(value: unknown): value is ObjectPro
|
|
|
672
729
|
typeof value.objectTypeId === "string" &&
|
|
673
730
|
typeof value.datasetId === "string" &&
|
|
674
731
|
isRecord(value.properties) &&
|
|
675
|
-
isRecord(value.links)
|
|
732
|
+
isRecord(value.links) &&
|
|
733
|
+
isSourceEditConflictResolution(value.conflictResolution)
|
|
676
734
|
)
|
|
677
735
|
}
|
|
678
736
|
|
|
737
|
+
function isSourceEditConflictResolution(value: unknown): boolean {
|
|
738
|
+
if (value === undefined) return true
|
|
739
|
+
if (!isRecord(value) || typeof value.strategy !== "string") return false
|
|
740
|
+
if (value.strategy === "editsWin") return value.sourceTimestamp === undefined
|
|
741
|
+
return value.strategy === "mostRecent" && typeof value.sourceTimestamp === "string"
|
|
742
|
+
}
|
|
743
|
+
|
|
679
744
|
/** Runtime type guard for {@link LinkProjectionDefinition}. */
|
|
680
745
|
export function isLinkProjectionDefinition(value: unknown): value is LinkProjectionDefinition {
|
|
681
746
|
if (!isRecord(value)) return false
|
package/src/projections/index.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
// ── Builders ─────────────────────────────────────────────────
|
|
2
2
|
|
|
3
|
+
export type {
|
|
4
|
+
ObjectProjectionBuilder,
|
|
5
|
+
ObjectProjectionConflictResolution,
|
|
6
|
+
ProjectionForeignKeyInput,
|
|
7
|
+
} from "./builders"
|
|
3
8
|
export {
|
|
4
9
|
categorizeProjections,
|
|
5
10
|
defineProjection,
|
|
@@ -24,6 +29,7 @@ export type {
|
|
|
24
29
|
ProjectionKind,
|
|
25
30
|
ProjectionTarget,
|
|
26
31
|
ProjectionTargetByKind,
|
|
32
|
+
SourceEditConflictResolution,
|
|
27
33
|
TelemetryProjectionDefinition,
|
|
28
34
|
} from "./types"
|
|
29
35
|
// ── Validation ───────────────────────────────────────────────
|
|
@@ -7,7 +7,9 @@ import type { ProjectionDispatchDescriptor } from "./types"
|
|
|
7
7
|
*
|
|
8
8
|
* `projectionTargetOf` derives the object types a projection writes, to record them on the run.
|
|
9
9
|
* `validateTelemetryProjectionFieldMapping` re-checks a telemetry mapping against the dataset schema
|
|
10
|
-
* before writing.
|
|
10
|
+
* before writing. `parseDatasetTimestamp` keeps worker timestamps aligned with the materializer's
|
|
11
|
+
* authoritative validation. These are plumbing, so they live here instead of on the
|
|
12
|
+
* `@sixb/core` root.
|
|
11
13
|
*/
|
|
12
14
|
export { projectionTargetOf } from "./builders"
|
|
13
15
|
export {
|
|
@@ -27,6 +29,7 @@ export {
|
|
|
27
29
|
ProjectionRunDispatcher,
|
|
28
30
|
} from "./run-dispatch"
|
|
29
31
|
export { createProjectionRunId } from "./run-id"
|
|
32
|
+
export { parseDatasetTimestamp } from "./timestamp"
|
|
30
33
|
export { validateTelemetryProjectionFieldMapping } from "./validation"
|
|
31
34
|
|
|
32
35
|
export type { ProjectionDispatchDescriptor, ProjectionMaterializationIdentity, ProjectionRegistry }
|
|
@@ -241,6 +241,13 @@ function cloneObjectProjectionDefinition(
|
|
|
241
241
|
},
|
|
242
242
|
])
|
|
243
243
|
),
|
|
244
|
+
conflictResolution:
|
|
245
|
+
projection.conflictResolution?.strategy === "mostRecent"
|
|
246
|
+
? {
|
|
247
|
+
strategy: "mostRecent",
|
|
248
|
+
sourceTimestamp: projection.conflictResolution.sourceTimestamp,
|
|
249
|
+
}
|
|
250
|
+
: { strategy: "editsWin" },
|
|
244
251
|
}
|
|
245
252
|
}
|
|
246
253
|
|
|
@@ -147,6 +147,7 @@ function normalizeProjectionDefinition(projection: ProjectionDefinition): JsonVa
|
|
|
147
147
|
},
|
|
148
148
|
])
|
|
149
149
|
),
|
|
150
|
+
conflictResolution: projection.conflictResolution ?? { strategy: "editsWin" },
|
|
150
151
|
}
|
|
151
152
|
}
|
|
152
153
|
if (projection._tag === "LinkProjectionDefinition") {
|
|
@@ -177,6 +178,9 @@ function projectionColumns(projection: ProjectionDefinition): Set<string> {
|
|
|
177
178
|
...Object.values(projection.links).flatMap((link) =>
|
|
178
179
|
link.sourceField === undefined ? [] : [link.sourceField]
|
|
179
180
|
),
|
|
181
|
+
...(projection.conflictResolution?.strategy === "mostRecent"
|
|
182
|
+
? [projection.conflictResolution.sourceTimestamp]
|
|
183
|
+
: []),
|
|
180
184
|
])
|
|
181
185
|
}
|
|
182
186
|
if (projection._tag === "LinkProjectionDefinition") {
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const DATASET_TIMESTAMP =
|
|
2
|
+
/^(\d{4})-(\d{1,2})-(\d{1,2})(?:[ T](\d{1,2}):(\d{2})(?::(\d{2})(?:\.(\d{1,9}))?)?)?([zZ]|[+-]\d{2}:?\d{2})?$/
|
|
3
|
+
|
|
4
|
+
interface TimestampComponents {
|
|
5
|
+
readonly year: number
|
|
6
|
+
readonly month: number
|
|
7
|
+
readonly day: number
|
|
8
|
+
readonly hour: number
|
|
9
|
+
readonly minute: number
|
|
10
|
+
readonly second: number
|
|
11
|
+
readonly milliseconds: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Parses a projection dataset timestamp, interpreting zone-less values as UTC. */
|
|
15
|
+
export function parseDatasetTimestamp(value: unknown): Date | null {
|
|
16
|
+
if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value
|
|
17
|
+
if (typeof value !== "string") return null
|
|
18
|
+
|
|
19
|
+
const match = DATASET_TIMESTAMP.exec(value.trim())
|
|
20
|
+
if (!match) return null
|
|
21
|
+
const [, year, month, day, hour, minute, second, fraction, zone] = match
|
|
22
|
+
const offsetMinutes = parseZoneOffsetMinutes(zone)
|
|
23
|
+
if (offsetMinutes === null) return null
|
|
24
|
+
|
|
25
|
+
const components: TimestampComponents = {
|
|
26
|
+
year: Number(year),
|
|
27
|
+
month: Number(month),
|
|
28
|
+
day: Number(day),
|
|
29
|
+
hour: hour === undefined ? 0 : Number(hour),
|
|
30
|
+
minute: minute === undefined ? 0 : Number(minute),
|
|
31
|
+
second: second === undefined ? 0 : Number(second),
|
|
32
|
+
milliseconds: fraction === undefined ? 0 : Math.trunc(Number(`0.${fraction}`) * 1000),
|
|
33
|
+
}
|
|
34
|
+
const wallClock = Date.UTC(
|
|
35
|
+
components.year,
|
|
36
|
+
components.month - 1,
|
|
37
|
+
components.day,
|
|
38
|
+
components.hour,
|
|
39
|
+
components.minute,
|
|
40
|
+
components.second,
|
|
41
|
+
components.milliseconds
|
|
42
|
+
)
|
|
43
|
+
if (!wallClockMatchesComponents(wallClock, components)) return null
|
|
44
|
+
return new Date(wallClock - offsetMinutes * 60_000)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseZoneOffsetMinutes(zone: string | undefined): number | null {
|
|
48
|
+
if (zone === undefined || zone === "Z" || zone === "z") return 0
|
|
49
|
+
const digits = zone.slice(1).replace(":", "")
|
|
50
|
+
const hours = Number(digits.slice(0, 2))
|
|
51
|
+
const minutes = Number(digits.slice(2, 4))
|
|
52
|
+
if (hours > 23 || minutes > 59) return null
|
|
53
|
+
return (zone[0] === "-" ? -1 : 1) * (hours * 60 + minutes)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function wallClockMatchesComponents(wallClock: number, components: TimestampComponents): boolean {
|
|
57
|
+
const date = new Date(wallClock)
|
|
58
|
+
return (
|
|
59
|
+
date.getUTCFullYear() === components.year &&
|
|
60
|
+
date.getUTCMonth() === components.month - 1 &&
|
|
61
|
+
date.getUTCDate() === components.day &&
|
|
62
|
+
date.getUTCHours() === components.hour &&
|
|
63
|
+
date.getUTCMinutes() === components.minute &&
|
|
64
|
+
date.getUTCSeconds() === components.second
|
|
65
|
+
)
|
|
66
|
+
}
|
package/src/projections/types.ts
CHANGED
|
@@ -39,6 +39,11 @@ export interface ForeignKeyDescriptor {
|
|
|
39
39
|
readonly targetObjectTypeId: string
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/** Resolves competing source-projection and managed-edit values for projected properties. */
|
|
43
|
+
export type SourceEditConflictResolution =
|
|
44
|
+
| { readonly strategy: "editsWin" }
|
|
45
|
+
| { readonly strategy: "mostRecent"; readonly sourceTimestamp: string }
|
|
46
|
+
|
|
42
47
|
/**
|
|
43
48
|
* Lowered, serializable definition for projecting a dataset into object instances.
|
|
44
49
|
*
|
|
@@ -56,6 +61,8 @@ export interface ObjectProjectionDefinition {
|
|
|
56
61
|
readonly properties: Readonly<Record<string, string>>
|
|
57
62
|
/** Maps link id -> FK descriptor. Empty `{}` when no FK links are projected. */
|
|
58
63
|
readonly links: Readonly<Record<string, ForeignKeyDescriptor>>
|
|
64
|
+
/** Defaults to `editsWin` when omitted by a legacy lowered definition. */
|
|
65
|
+
readonly conflictResolution?: SourceEditConflictResolution
|
|
59
66
|
}
|
|
60
67
|
|
|
61
68
|
/**
|
|
@@ -315,6 +315,30 @@ export function validateProjectionsAtStartup(
|
|
|
315
315
|
}
|
|
316
316
|
|
|
317
317
|
const datasetColumnNames = new Set(dataset.schema.columns.map((column) => column.name))
|
|
318
|
+
const conflictResolution = projection.conflictResolution ?? { strategy: "editsWin" }
|
|
319
|
+
if (conflictResolution.strategy === "mostRecent") {
|
|
320
|
+
const sourceTimestamp = dataset.schema.columns.find(
|
|
321
|
+
(column) => column.name === conflictResolution.sourceTimestamp
|
|
322
|
+
)
|
|
323
|
+
if (!sourceTimestamp) {
|
|
324
|
+
throw new ProjectionValidationError(
|
|
325
|
+
`${prefix}: source timestamp "${conflictResolution.sourceTimestamp}" references unknown ` +
|
|
326
|
+
`dataset column on dataset "${projection.datasetId}"`
|
|
327
|
+
)
|
|
328
|
+
}
|
|
329
|
+
if (sourceTimestamp.type !== "timestamp") {
|
|
330
|
+
throw new ProjectionValidationError(
|
|
331
|
+
`${prefix}: source timestamp "${conflictResolution.sourceTimestamp}" must reference a ` +
|
|
332
|
+
`timestamp dataset column`
|
|
333
|
+
)
|
|
334
|
+
}
|
|
335
|
+
if (sourceTimestamp.nullable === true) {
|
|
336
|
+
throw new ProjectionValidationError(
|
|
337
|
+
`${prefix}: source timestamp "${conflictResolution.sourceTimestamp}" must reference a ` +
|
|
338
|
+
`non-null timestamp dataset column`
|
|
339
|
+
)
|
|
340
|
+
}
|
|
341
|
+
}
|
|
318
342
|
const propertyIds = new Set(objectType.properties.map((p) => p.id))
|
|
319
343
|
for (const [propId, columnName] of Object.entries(projection.properties)) {
|
|
320
344
|
if (!propertyIds.has(propId)) {
|
package/src/queues/index.ts
CHANGED