@dotbep/core 0.2.7 → 0.2.8

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 (77) hide show
  1. package/dist/index.d.ts +7 -5
  2. package/dist/index.js +604 -596
  3. package/package.json +4 -1
  4. package/examples/01-participants.ts +0 -127
  5. package/examples/02-files.ts +0 -100
  6. package/examples/03-workflows.ts +0 -149
  7. package/examples/04-bim-uses.ts +0 -70
  8. package/examples/05-standards.ts +0 -60
  9. package/examples/06-schedule.ts +0 -124
  10. package/examples/07-loin.ts +0 -133
  11. package/examples/08-deliverables.ts +0 -126
  12. package/examples/09-notes.ts +0 -73
  13. package/examples/10-llm.ts +0 -109
  14. package/examples/11-resolved.ts +0 -133
  15. package/examples/12-history.ts +0 -166
  16. package/examples/13-engine.ts +0 -152
  17. package/examples/bep.d.ts +0 -38
  18. package/examples/example.bep +0 -0
  19. package/examples/run-all.ts +0 -38
  20. package/src/base/entity.ts +0 -148
  21. package/src/base/history.ts +0 -497
  22. package/src/base/index.ts +0 -5
  23. package/src/base/singleton.ts +0 -26
  24. package/src/entities/actions.ts +0 -25
  25. package/src/entities/adapters.ts +0 -16
  26. package/src/entities/annexes.ts +0 -17
  27. package/src/entities/assetTypes.ts +0 -30
  28. package/src/entities/automations.ts +0 -24
  29. package/src/entities/bimUses.ts +0 -50
  30. package/src/entities/deliverables.ts +0 -66
  31. package/src/entities/disciplines.ts +0 -21
  32. package/src/entities/effects.ts +0 -28
  33. package/src/entities/env.ts +0 -17
  34. package/src/entities/events.ts +0 -24
  35. package/src/entities/extensions.ts +0 -16
  36. package/src/entities/flags.ts +0 -17
  37. package/src/entities/guides.ts +0 -26
  38. package/src/entities/index.ts +0 -32
  39. package/src/entities/lbsNodes.ts +0 -193
  40. package/src/entities/lods.ts +0 -22
  41. package/src/entities/loin.ts +0 -127
  42. package/src/entities/lois.ts +0 -22
  43. package/src/entities/members.ts +0 -137
  44. package/src/entities/milestones.ts +0 -32
  45. package/src/entities/notes.ts +0 -27
  46. package/src/entities/objectives.ts +0 -17
  47. package/src/entities/phases.ts +0 -17
  48. package/src/entities/remoteData.ts +0 -17
  49. package/src/entities/resolvers.ts +0 -20
  50. package/src/entities/roles.ts +0 -29
  51. package/src/entities/softwares.ts +0 -26
  52. package/src/entities/standards.ts +0 -68
  53. package/src/entities/teams.ts +0 -42
  54. package/src/entities/workflows.ts +0 -256
  55. package/src/index.ts +0 -464
  56. package/src/runtime/Engine.ts +0 -352
  57. package/src/runtime/MemoryStorage.ts +0 -31
  58. package/src/runtime/Runtime.ts +0 -106
  59. package/src/runtime/index.ts +0 -4
  60. package/src/runtime/transitions.ts +0 -456
  61. package/src/runtime/types.ts +0 -279
  62. package/src/types/history.ts +0 -37
  63. package/src/types/index.ts +0 -24
  64. package/src/types/resolved.ts +0 -137
  65. package/src/types/schema.ts +0 -757
  66. package/src/utils/diff.ts +0 -109
  67. package/src/utils/index.ts +0 -9
  68. package/src/utils/integrity.ts +0 -108
  69. package/src/utils/lbs.ts +0 -116
  70. package/src/utils/mermaid.ts +0 -110
  71. package/src/utils/naming.ts +0 -62
  72. package/src/utils/nomenclature.ts +0 -107
  73. package/src/utils/normalize.ts +0 -35
  74. package/src/utils/raci.ts +0 -25
  75. package/src/utils/textFile.ts +0 -24
  76. package/tsconfig.json +0 -12
  77. package/vite.config.ts +0 -24
@@ -1,166 +0,0 @@
1
- // part of: node --experimental-strip-types examples/run-all.ts (use --12 to stop here)
2
- //
3
- // Covers: history — commit, status, hasPendingChanges, discard, compare,
4
- // get, getStandardContent, revert, reset, squash.
5
- //
6
- // The history system versions the BEP using inverse RFC 6902 diffs. Each
7
- // commit stores how to go back to the previous version, so the current state
8
- // is always bep.json and older states are reconstructed on demand.
9
- //
10
- // Version numbers follow a {major}.{minor} scheme:
11
- // patch commit → bumps minor (0.0 → 0.1 → 0.2)
12
- // version commit → bumps major (0.x → 1.0), requires approvedBy[]
13
- //
14
- // Bep.open() auto-normalizes a BEP with no history by silently creating a v0.0
15
- // baseline from the current state. The first manual commit therefore lands at
16
- // v0.1, not v0.0. This example operates in memory only — it does not write back
17
- // to example.bep, so the history modifications are discarded after the run.
18
- //
19
- // Standards (.md files) are versioned separately from bep.json: the history
20
- // system snapshots each .md file only when it changes between commits, and
21
- // resolves the correct snapshot when getStandardContent(id, version) is called.
22
-
23
- import { readFileSync } from 'node:fs'
24
- import { Bep } from '../dist/index.js'
25
-
26
- const bep = await Bep.open(readFileSync('examples/example.bep'))
27
-
28
- const stdNamingId = bep.standards.list().find(s => s.name === 'Naming Convention')!.id
29
-
30
- // ─── v0.1: first manual commit ────────────────────────────────────────────────
31
- //
32
- // Bep.open() auto-normalizes a BEP without history by creating a v0.0 baseline
33
- // from the current state. The first manual commit therefore lands at v0.1.
34
-
35
- console.log('=== history ===')
36
- console.log('\n--- commit v0.1 (initial) ---')
37
-
38
- const v0 = await bep.history.commit({
39
- type: 'patch',
40
- author: 'alice@arc.com',
41
- description: 'Initial BEP draft — all sections complete',
42
- })
43
- console.log('committed:', v0.version) // 0.1
44
-
45
- // ─── v0.2: data changes + standard content update ─────────────────────────────
46
-
47
- console.log('\n--- changes for v0.2 ---')
48
-
49
- bep.roles.add([{ name: 'BIM Author', color: '#22CC88' }])
50
- bep.standards.setContent(stdNamingId, '# Naming Convention v3\nRevised rules after client review.')
51
-
52
- // status() reports what changed since the last commit
53
- const st02 = await bep.history.status()
54
- console.log('pending — standards:', st02.standards)
55
- // → [{ status: 'content-modified', id: stdNamingId }]
56
-
57
- const v1 = await bep.history.commit({
58
- type: 'patch',
59
- author: 'alice@arc.com',
60
- description: 'Add BIM Author role + revise naming standard',
61
- })
62
- console.log('committed:', v1.version) // 0.2
63
-
64
- // resolve standard content at specific historical versions
65
- const namingAtV01 = await bep.history.getStandardContent(stdNamingId, '0.1')
66
- const namingAtV02 = await bep.history.getStandardContent(stdNamingId, '0.2')
67
- console.log('naming @ v0.1 (first 35):', namingAtV01?.slice(0, 35))
68
- console.log('naming @ v0.2 (first 35):', namingAtV02?.slice(0, 35))
69
-
70
- // ─── v1.0: official version with approvers ────────────────────────────────────
71
-
72
- console.log('\n--- commit v1.0 (official version, requires approvedBy) ---')
73
-
74
- bep.standards.setContent(stdNamingId, '# Naming Convention v4\nFinal approved version.')
75
- bep.phases.add([{ name: 'Tender' }])
76
-
77
- const v2 = await bep.history.commit({
78
- type: 'version',
79
- author: 'alice@arc.com',
80
- description: 'First official BEP release',
81
- approvedBy: ['alice@arc.com'],
82
- })
83
- console.log('committed:', v2.version) // 1.0
84
-
85
- // ─── discard ──────────────────────────────────────────────────────────────────
86
-
87
- console.log('\n--- discard uncommitted changes ---')
88
-
89
- bep.phases.add([{ name: 'Temporary — will be discarded' }])
90
- bep.standards.setContent(stdNamingId, '# WILL BE DISCARDED')
91
-
92
- console.log('hasPendingChanges before discard:', await bep.history.hasPendingChanges())
93
- await bep.history.discard()
94
- console.log('hasPendingChanges after discard: ', await bep.history.hasPendingChanges())
95
-
96
- const namingAfterDiscard = await bep.standards.getContent(stdNamingId)
97
- console.log('naming std restored (first 35):', namingAfterDiscard.slice(0, 35))
98
- // → v4 content (restored from baseline at v1.0)
99
-
100
- // ─── list / get / compare ─────────────────────────────────────────────────────
101
-
102
- console.log('\n--- list / get / compare ---')
103
-
104
- const versions = await bep.history.list()
105
- console.log('versions:', versions.map(v => v.version)) // ['0.1', '0.2', '1.0']
106
-
107
- // get: reconstruct state at v0.1 — BIM Author role should not exist yet
108
- const atV01 = await bep.history.get('0.1')
109
- const bimAuthorInV01 = atV01.roles.find(r => r.name === 'BIM Author')
110
- console.log('BIM Author in v0.1:', bimAuthorInV01 ?? 'not found (correct)')
111
-
112
- // compare: RFC 6902 diff between two versions + standards diff
113
- const { diff: ops, standards: stdDiff } = await bep.history.compare('0.1', '0.2')
114
- console.log('ops v0.1→v0.2:', ops.length, 'operation(s)', ops.slice(0, 2).map(o => `${o.op} ${o.path}`))
115
- console.log('std diff: ', stdDiff)
116
-
117
- // ─── revert ───────────────────────────────────────────────────────────────────
118
-
119
- console.log('\n--- revert to v0.0 (non-destructive — creates a new version) ---')
120
-
121
- const v3 = await bep.history.revert('0.1', {
122
- type: 'patch',
123
- author: 'alice@arc.com',
124
- description: 'Revert to initial draft',
125
- })
126
- console.log('reverted as:', v3.version) // 1.1
127
-
128
- const bimAuthorAfterRevert = bep.roles.list().find(r => r.name === 'BIM Author')
129
- console.log('BIM Author after revert:', bimAuthorAfterRevert ?? 'not found (correct)')
130
- console.log('versions after revert: ', (await bep.history.list()).map(v => v.version))
131
- // → ['0.0', '0.1', '0.2', '1.0', '1.1']
132
-
133
- // ─── reset ────────────────────────────────────────────────────────────────────
134
-
135
- console.log('\n--- reset to v0.2 (destructive — deletes all versions after v0.2) ---')
136
-
137
- console.log('versions before reset:', (await bep.history.list()).map(v => v.version))
138
- await bep.history.reset('0.2')
139
- console.log('versions after reset: ', (await bep.history.list()).map(v => v.version))
140
- // → ['0.0', '0.1', '0.2']
141
-
142
- // ─── squash ───────────────────────────────────────────────────────────────────
143
-
144
- console.log('\n--- squash (destructive — collapses all history into one terminus) ---')
145
-
146
- console.log('versions before squash:', (await bep.history.list()).map(v => v.version))
147
-
148
- const squashed = await bep.history.squash({
149
- newBase: '2.0',
150
- author: 'alice@arc.com',
151
- description: 'Clean start — squash all history into v2.0',
152
- approvedBy: ['alice@arc.com'],
153
- })
154
- console.log('squash result:', squashed.version)
155
- console.log('versions after squash:', (await bep.history.list()).map(v => v.version))
156
- // → ['2.0']
157
-
158
- // getStandardContent still works from the new terminus
159
- const namingAt20 = await bep.history.getStandardContent(stdNamingId, '2.0')
160
- console.log('\nnaming std @ 2.0 (first 35):', namingAt20?.slice(0, 35))
161
-
162
- // get() terminus works
163
- const atSquashed = await bep.history.get('2.0')
164
- console.log('get 2.0 — project name:', atSquashed.project.name)
165
-
166
- console.log('\nDone.')
@@ -1,152 +0,0 @@
1
- // run: node --experimental-strip-types examples/13-engine.ts (from core/)
2
- //
3
- // Covers: Engine, Runtime, workflow execution, type generation,
4
- // getRemoteData, useAdapter.
5
- //
6
- // bep.generateRuntimeTypes() produces a TypeScript contract from the BEP's runtime handlers — with JSDoc from each description field.
7
- // Writing it to bep.d.ts gives full type safety in the Runtime development.
8
- //
9
- // Workflow (3 nodes):
10
- //
11
- // start ──► review ──[submit / effect: notify-reviewer]──► end
12
-
13
- import { writeFileSync } from 'node:fs'
14
- import * as BEP from '../dist/index.js'
15
- import type { BepTypes } from './bep.js'
16
-
17
- // ─── 1. Build the BEP ─────────────────────────────────────────────────────────
18
-
19
- const bep = BEP.Bep.create({ name: 'Demo Project', code: 'DEMO', description: '' })
20
-
21
- const [{ id: roleManagerId }] = bep.roles.add([{ name: 'BIM Manager' }]).succeeded
22
- bep.members.add([{ email: 'manager@demo.com', name: 'Ana García', roleId: roleManagerId }])
23
- const [{ id: assetTypeId }] = bep.assetTypes.add([{ id: 'MDL', name: 'Model' }]).succeeded
24
- const [{ id: actionReviewId }] = bep.actions.add([{ name: 'Review model' }]).succeeded
25
-
26
- bep.events.add([
27
- { id: 'submit', name: 'Submit for review', payload: [{ key: 'comment', type: 'string', required: false }] },
28
- { id: 'approved', name: 'Approved' },
29
- ])
30
- bep.effects.add([
31
- {
32
- id: 'notify-reviewer', name: 'Notify reviewer',
33
- description: 'Sends a notification to the assigned reviewer when a model is submitted. Uses the comment from the submit event payload.',
34
- payload: [{ key: 'comment', type: 'string', required: false }],
35
- },
36
- ])
37
- bep.automations.add([
38
- {
39
- id: 'auto-approve', name: 'Auto approve',
40
- description: 'Automatically approves the model if no blocking issues are found in the previous review cycle.',
41
- payload: [{ key: 'threshold', type: 'number', required: true }],
42
- output: [{ key: 'result', type: 'string', required: true }],
43
- },
44
- ])
45
- bep.resolvers.add([
46
- {
47
- id: 'fetch-json', name: 'Fetch JSON',
48
- description: 'Fetches a JSON array from the remote data URL. Authenticates with an API key via Authorization header. Returns the raw parsed array.',
49
- envKeys: ['API_KEY'],
50
- },
51
- ])
52
- bep.adapters.add([
53
- {
54
- id: 'pick-label-value', name: 'Pick label + value',
55
- description: 'Maps an array of { name, count } objects to { label, value } pairs compatible with dotbep:pie-chart.',
56
- },
57
- ])
58
- bep.remoteData.add([
59
- {
60
- name: 'Model stats', url: 'https://example.com/stats.json',
61
- description: 'Aggregated model statistics exported nightly from the project management tool.',
62
- resolverId: 'fetch-json',
63
- },
64
- ])
65
-
66
- const [{ id: workflowId }] = bep.workflows.add([{
67
- name: 'Model Review',
68
- diagram: {
69
- direction: 'LR',
70
- nodes: {
71
- start: { type: 'start' },
72
- review: { type: 'process', actionId: actionReviewId, responsibleRoleIds: [roleManagerId] },
73
- end: { type: 'end' },
74
- },
75
- edges: {
76
- e1: { from: 'start', to: 'review' },
77
- e2: { from: 'review', to: 'end', triggerEventId: 'submit', effectIds: ['notify-reviewer'] },
78
- },
79
- },
80
- }]).succeeded
81
-
82
- // ─── 2. Generate types ────────────────────────────────────────────────────────
83
- //
84
- // bep.generateRuntimeTypes() produces a TypeScript contract from the BEP's effects and
85
- // automations. Commit bep.d.ts alongside your runtime so TypeScript can validate it.
86
-
87
- writeFileSync('examples/bep.d.ts', bep.generateRuntimeTypes())
88
- console.log('Generated examples/bep.d.ts')
89
-
90
- // ─── 3. Declare the BEP Runtime ───────────────────────────────────────────────
91
- //
92
- // BepTypes (from bep.d.ts) types each handler's payload automatically.
93
-
94
- class MyRuntime extends BEP.Runtime<BepTypes> {
95
- constructor(options: BEP.RuntimeOptions) {
96
- super(options)
97
- this.effect('notify-reviewer', async (instance, payload) => {
98
- console.log(' [effect] notify-reviewer fired')
99
- console.log(' [effect] submitted by:', instance.history.at(-1)?.actor)
100
- console.log(' [effect] comment:', payload.comment) // string | undefined ← inferred
101
- })
102
- this.automation('auto-approve', async (_instance, payload) => {
103
- console.log(' [automation] auto-approve running, threshold:', payload.threshold)
104
- return { eventId: 'approved', result: 'passed' }
105
- })
106
- this.resolver('fetch-json', async (url, env) => {
107
- const res = await fetch(url, { headers: { Authorization: `Bearer ${env.API_KEY}` } })
108
- return res.json()
109
- })
110
- this.adapter('pick-label-value', (data) => {
111
- return (data as { name: string; count: number }[]).map(d => ({ label: d.name, value: d.count }))
112
- })
113
- }
114
- }
115
-
116
- // ─── 4. Init the engine and run ───────────────────────────────────────────────
117
-
118
- bep.engine.init({ runtime: new MyRuntime({ env: {} }) })
119
-
120
- console.log('\n=== create instance ===')
121
- const instance = await bep.engine.createInstance(
122
- workflowId,
123
- { assetTypeId, id: 'model-001', label: 'Structural Model v3', source: 'bep:deliverables' },
124
- 'manager@demo.com',
125
- )
126
- console.log('status:', instance!.status)
127
- console.log('current node:', instance!.currentNodeId)
128
-
129
- console.log('\n=== emit: submit ===')
130
- const result = await bep.engine.emit(instance!.id, {
131
- eventId: 'submit',
132
- actor: 'manager@demo.com',
133
- payload: { comment: 'Ready for review' },
134
- })
135
- console.log('ok:', result.ok)
136
- console.log('transitions:', result.transitionsApplied?.map(t => `${t.fromNodeId} → ${t.toNodeId}`))
137
- console.log('effects:', result.effects?.map(e => `${e.effectId}: ${e.status}`))
138
- console.log('final node:', result.instance?.currentNodeId)
139
- console.log('final status:', result.instance?.status)
140
-
141
- console.log('\n=== getRemoteData + useAdapter ===')
142
- // Uses a mock resolver so the demo works without a real endpoint.
143
- const remoteDataId = bep.data.remoteData[0]!.id
144
- try {
145
- const raw = await bep.engine.getRemoteData(remoteDataId)
146
- const adapted = bep.engine.useAdapter('pick-label-value', raw)
147
- console.log('raw data:', raw)
148
- console.log('adapted:', adapted)
149
- } catch (err) {
150
- // example.com/stats.json returns HTML — expected in this demo.
151
- console.log('(resolver error, expected with placeholder URL):', (err as Error).message)
152
- }
package/examples/bep.d.ts DELETED
@@ -1,38 +0,0 @@
1
- // Generated by bep.generateTypes() — do not edit manually
2
-
3
- // ─── Effects ──────────────────────────────────────────────────────────────────
4
-
5
- export interface BepEffects {
6
- /** Sends a notification to the assigned reviewer when a model is submitted. Uses the comment from the submit event payload. */
7
- 'notify-reviewer': (payload: { comment?: string }) => void
8
- }
9
-
10
- // ─── Automations ─────────────────────────────────────────────────────────────
11
-
12
- export interface BepAutomations {
13
- /** Automatically approves the model if no blocking issues are found in the previous review cycle. */
14
- 'auto-approve': (payload: { threshold: number }) => { eventId: string; result: string }
15
- }
16
-
17
- // ─── Resolvers ────────────────────────────────────────────────────────────────
18
-
19
- export interface BepResolvers {
20
- /** Fetches a JSON array from the remote data URL. Authenticates with an API key via Authorization header. Returns the raw parsed array. */
21
- 'fetch-json': (url: string, env: { API_KEY: string }) => unknown
22
- }
23
-
24
- // ─── Adapters ─────────────────────────────────────────────────────────────────
25
-
26
- export interface BepAdapters {
27
- /** Maps an array of { name, count } objects to { label, value } pairs compatible with dotbep:pie-chart. */
28
- 'pick-label-value': (data: unknown) => unknown
29
- }
30
-
31
- // ─── Combined ─────────────────────────────────────────────────────────────────
32
-
33
- export interface BepTypes {
34
- effects: BepEffects
35
- automations: BepAutomations
36
- resolvers: BepResolvers
37
- adapters: BepAdapters
38
- }
Binary file
@@ -1,38 +0,0 @@
1
- // run: node --experimental-strip-types examples/run-all.ts (from core/)
2
- // Runs all examples in sequence. Each example builds on the .bep saved by the previous one.
3
- // Optional: --NN runs only examples 01 through NN (e.g. --05 stops after 05-standards).
4
- import { execSync } from 'node:child_process'
5
-
6
- const examples = [
7
- '01-participants',
8
- '02-files',
9
- '03-workflows',
10
- '04-bim-uses',
11
- '05-standards',
12
- '06-schedule',
13
- '07-loin',
14
- '08-deliverables',
15
- '09-notes',
16
- '10-llm',
17
- '11-resolved',
18
- '12-history',
19
- ]
20
-
21
- const untilArg = process.argv.find(a => /^--\d{2}$/.test(a))
22
- const untilPrefix = untilArg ? untilArg.slice(2) : null
23
- const toRun = untilPrefix ? examples.filter(e => e.slice(0, 2) <= untilPrefix) : examples
24
-
25
- for (const ex of toRun) {
26
- console.log(`\n${'─'.repeat(60)}`)
27
- console.log(` examples/${ex}.ts`)
28
- console.log('─'.repeat(60))
29
- execSync(`node --experimental-strip-types examples/${ex}.ts`, {
30
- stdio: 'inherit',
31
- cwd: process.cwd(),
32
- env: { ...process.env, DOTBEP_RUN_ALL: '1' },
33
- })
34
- }
35
-
36
- console.log(`\n${'─'.repeat(60)}`)
37
- console.log(untilPrefix ? ` Done (up to ${untilPrefix}).` : ' All examples completed.')
38
- console.log('─'.repeat(60))
@@ -1,148 +0,0 @@
1
- import { z, ZodError } from 'zod'
2
- import type { BEP } from '../types/schema.js'
3
- import { type ArrayKeys, checkRefs, checkOutgoingRefs } from '../utils/integrity.js'
4
-
5
- function errMsg(e: unknown): string {
6
- if (e instanceof ZodError)
7
- return e.issues.map(i => (i.path.length ? i.path.join('.') + ': ' : '') + i.message).join('; ')
8
- return (e as Error).message
9
- }
10
-
11
- export type BulkResult<T> = {
12
- succeeded: T[]
13
- failed: { id: string; error: string }[]
14
- }
15
-
16
- /** Input type for add() on non-autoId entities — id is optional, validated by the Zod schema */
17
- export type AddInput<T extends object> = Omit<T, 'id'> & { id?: string }
18
-
19
- export type EntityConfig<T extends object, AutoId extends boolean = false> = {
20
- /** Key of this entity's array in BEP — used for automatic referential integrity checks */
21
- key: ArrayKeys<BEP>
22
- /** Field used as the unique identifier. Defaults to 'id' */
23
- idField?: keyof T
24
- /** Zod schema for this entity — used to validate input on add and merged result on update */
25
- schema: z.ZodType<T>
26
- /**
27
- * When true, a UUID is auto-generated for the id field.
28
- * add() will not accept an id in its input — callers capture it from the result.
29
- * Use for entities whose id is opaque (not part of nomenclature and not user-facing).
30
- */
31
- autoId?: AutoId
32
- /**
33
- * Custom validation called after schema parse (on add) and after merge (on update).
34
- * Use for nested reference checks not covered by the standard integrity rules.
35
- * Returns a list of error messages; empty array means valid.
36
- */
37
- validate?: (item: T, bep: BEP) => string[]
38
- /** Called before removing an entity, after ref checks pass. Use for custom logic not covered by integrity rules. */
39
- beforeRemove?: (id: string, bep: BEP) => void
40
- }
41
-
42
- export class Entity<T extends object, AutoId extends boolean = false> {
43
- private idField: keyof T
44
-
45
- constructor(
46
- private getItems: () => T[],
47
- protected getBep: () => BEP,
48
- private config: EntityConfig<T, AutoId>,
49
- ) {
50
- this.idField = config.idField ?? ('id' as keyof T)
51
- }
52
-
53
- private getId(entity: T): string {
54
- return String(entity[this.idField])
55
- }
56
-
57
- list(): T[] {
58
- return this.getItems()
59
- }
60
-
61
- get(ids: string[]): BulkResult<T> {
62
- const succeeded: T[] = []
63
- const failed: { id: string; error: string }[] = []
64
- for (const id of ids) {
65
- const entity = this.getItems().find(e => this.getId(e) === id)
66
- if (entity) succeeded.push(entity)
67
- else failed.push({ id, error: `Not found: ${id}` })
68
- }
69
- return { succeeded, failed }
70
- }
71
-
72
- add(inputs: (AutoId extends true ? Omit<T, 'id'> : AddInput<T>)[]): BulkResult<T> {
73
- const succeeded: T[] = []
74
- const failed: { id: string; error: string }[] = []
75
- for (const input of inputs) {
76
- const raw = { ...input } as Record<string, unknown>
77
- if (this.config.autoId && !raw[this.idField as string])
78
- raw[this.idField as string] = globalThis.crypto.randomUUID()
79
- const id = String(raw[this.idField as string] ?? '(unknown)')
80
- try {
81
- const entity = this.config.schema.parse(raw)
82
- if (this.getItems().some(e => this.getId(e) === this.getId(entity)))
83
- throw new Error(`Already exists: ${this.getId(entity)}`)
84
- const outgoing = checkOutgoingRefs(entity as Record<string, unknown>, this.config.key, this.getBep())
85
- if (outgoing.length) throw new Error(outgoing.join('; '))
86
- const custom = this.config.validate?.(entity, this.getBep()) ?? []
87
- if (custom.length) throw new Error(custom.join('; '))
88
- this.getItems().push(entity)
89
- succeeded.push(entity)
90
- } catch (e) {
91
- failed.push({ id, error: errMsg(e) })
92
- }
93
- }
94
- return { succeeded, failed }
95
- }
96
-
97
- update(patches: ({ [K in keyof T]?: T[K] } & Record<string, unknown>)[]): BulkResult<T> {
98
- const succeeded: T[] = []
99
- const failed: { id: string; error: string }[] = []
100
- for (const patch of patches) {
101
- const id = String(patch[this.idField as string])
102
- const items = this.getItems()
103
- const index = items.findIndex(e => this.getId(e) === id)
104
- if (index === -1) {
105
- failed.push({ id, error: `Not found: ${id}` })
106
- continue
107
- }
108
- const outgoing = checkOutgoingRefs(patch as Record<string, unknown>, this.config.key, this.getBep())
109
- if (outgoing.length) {
110
- failed.push({ id, error: outgoing.join('; ') })
111
- continue
112
- }
113
- try {
114
- const merged = this.config.schema.parse({ ...items[index], ...patch })
115
- const custom = this.config.validate?.(merged, this.getBep()) ?? []
116
- if (custom.length) throw new Error(custom.join('; '))
117
- items[index] = merged
118
- succeeded.push(items[index])
119
- } catch (e) {
120
- failed.push({ id, error: errMsg(e) })
121
- }
122
- }
123
- return { succeeded, failed }
124
- }
125
-
126
- remove(ids: string[]): BulkResult<string> {
127
- const succeeded: string[] = []
128
- const failed: { id: string; error: string }[] = []
129
- for (const id of ids) {
130
- const items = this.getItems()
131
- const index = items.findIndex(e => this.getId(e) === id)
132
- if (index === -1) {
133
- failed.push({ id, error: `Not found: ${id}` })
134
- continue
135
- }
136
- try {
137
- const active = checkRefs(id, this.config.key, this.getBep())
138
- if (active.length) throw new Error(`Referenced by: ${active.join(', ')}`)
139
- this.config.beforeRemove?.(id, this.getBep())
140
- items.splice(index, 1)
141
- succeeded.push(id)
142
- } catch (e) {
143
- failed.push({ id, error: errMsg(e) })
144
- }
145
- }
146
- return { succeeded, failed }
147
- }
148
- }