gdc-common-utils-ts 1.15.0 → 1.16.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.
@@ -1,88 +1,134 @@
1
- import { buildEmployeeBatchBundle, buildEmployeeBatchEntry, buildEmployeeSearchBundle, EmployeeBundleOperations } from './employee';
1
+ import { buildEmployeeBatchEntry, buildEmployeePurgeBundle, buildEmployeeSearchBundle, EmployeeBundleOperations, EmployeeClaims, EmployeeResourceTypes } from './employee';
2
2
  export type BundleOperation = (typeof EmployeeBundleOperations)[keyof typeof EmployeeBundleOperations];
3
- export type BuiltEmployeeBatchEntry = ReturnType<typeof buildEmployeeBatchEntry> & {
3
+ export type AllowedResourceType = (typeof EmployeeResourceTypes)[keyof typeof EmployeeResourceTypes];
4
+ export type BuiltBundleEntry = ReturnType<typeof buildEmployeeBatchEntry> & {
4
5
  fullUrl?: string;
5
6
  };
6
7
  /**
7
- * Generic bundle editor with an employee adapter surface.
8
+ * Generic bundle editor for FHIR-like bundle payloads assembled in memory.
8
9
  *
9
- * Current scope:
10
- * - one declared business operation per bundle
11
- * - active-entry editing through generic claim helpers
12
- * - employee convenience setters layered on the same active entry
10
+ * This class owns bundle-level concerns:
11
+ * - which business operation the bundle represents
12
+ * - which resource type the bundle allows
13
+ * - which entries belong to the bundle
14
+ * - when the final bundle is materialized through `build()`
13
15
  *
14
- * This editor lives in `common-utils` because it is runtime-neutral and can be
15
- * consumed by frontend SDKs, backend SDKs, and backend services.
16
+ * Resource-specific semantics belong to resource entry editors such as
17
+ * `EmployeeEntryEditor`.
16
18
  */
17
19
  export declare class BundleEditor {
18
20
  private bundleOperation;
21
+ private allowedResourceType;
19
22
  private readonly entries;
20
- private activeEntryIndex;
21
- /** Declares which business operation the bundle is assembling. */
23
+ /** Declares which business operation this bundle is staging. */
22
24
  setBundleOperation(operation: BundleOperation): this;
23
- /** Returns the operation currently assigned to this bundle. */
25
+ /** Returns the current business operation assigned to the bundle. */
24
26
  getBundleOperation(): BundleOperation | null;
25
27
  /**
26
- * Opens one new active entry.
28
+ * Restricts the bundle to one resource type.
27
29
  *
28
- * If the current bundle operation needs an identifier and none is supplied,
29
- * a canonical `urn:uuid:*` identifier is generated and aligned across
30
- * `fullUrl`, `resource.id`, and `org.schema.Person.identifier`.
30
+ * For employee batch bundles this should be fixed to
31
+ * `EmployeeResourceTypes.employee` so the editor cannot mix unrelated
32
+ * resource kinds.
31
33
  */
32
- newEntry(resourceId?: string): this;
33
- /** Reopens an existing entry by identifier or `fullUrl`. */
34
- openEntry(resourceId: string): this;
35
- /** Closes the current active entry while preserving it inside the bundle draft. */
36
- doneEntry(): this;
37
- /** Returns a cloned snapshot of the currently staged entries. */
38
- getEntries(): readonly BuiltEmployeeBatchEntry[];
34
+ setAllowedResourceType(resourceType: AllowedResourceType): this;
35
+ /** Returns the bundle resource type restriction when already declared. */
36
+ getAllowedResourceType(): AllowedResourceType | null;
39
37
  /**
40
- * Builds the final bundle payload for the declared operation.
38
+ * Opens one new entry and returns a generic entry editor for that slot.
41
39
  *
42
- * `search` returns one canonical search bundle.
43
- * `purge` returns a batch bundle whose entries are routed later to
44
- * `Employee/_purge`.
45
- * `create` and `disable` return canonical batch bundles.
40
+ * The entry editor can later expose resource-specific semantics through
41
+ * methods such as `asEmployee()`.
46
42
  */
47
- build(): ReturnType<typeof buildEmployeeBatchBundle> | ReturnType<typeof buildEmployeeSearchBundle>;
48
- /** Reads one claim from the active entry. */
43
+ newEntry(resourceId?: string): BundleEntryEditor;
44
+ /** Reopens one existing entry by `resource.id` or `fullUrl`. */
45
+ openEntry(resourceIdOrFullUrl: string): BundleEntryEditor;
46
+ /** Returns cloned staged entries for inspection or debugging. */
47
+ getEntries(): readonly BuiltBundleEntry[];
48
+ /**
49
+ * Materializes the final bundle payload from the editor state.
50
+ *
51
+ * `build()` does not send, sign, or wrap the payload. It only returns the
52
+ * final bundle object for the declared operation and staged entries.
53
+ */
54
+ build(): ReturnType<typeof buildEmployeePurgeBundle> | ReturnType<typeof buildEmployeeSearchBundle>;
55
+ /** @internal */
56
+ getMutableEntry(entryIndex: number): BuiltBundleEntry;
57
+ private createEntryDraft;
58
+ private getSingleSearchClaims;
59
+ private requireBundleOperation;
60
+ private requireAllowedResourceType;
61
+ }
62
+ /**
63
+ * Generic editor for one staged bundle entry.
64
+ *
65
+ * This class only knows generic entry concerns such as:
66
+ * - resource id
67
+ * - fullUrl
68
+ * - meta claims
69
+ * - conversion to one resource-specific entry editor
70
+ */
71
+ export declare class BundleEntryEditor {
72
+ protected readonly bundleEditor: BundleEditor;
73
+ protected readonly entryIndex: number;
74
+ constructor(bundleEditor: BundleEditor, entryIndex: number);
75
+ /** Opens the current entry as one employee-specific resource editor. */
76
+ asEmployee(): EmployeeEntryEditor;
77
+ /** Reads one claim from this entry. */
49
78
  getClaim(key: string): unknown;
50
- /** Checks whether the active entry contains one claim key. */
79
+ /** Checks whether this entry contains one claim key. */
51
80
  hasClaim(key: string): boolean;
52
- /** Writes one claim on the active entry. */
81
+ /** Writes one claim on this entry. */
53
82
  setClaim(key: string, value: unknown): this;
54
- /** Appends one claim value on the active entry. */
83
+ /** Appends one claim value on this entry. */
55
84
  addClaim(key: string, value: unknown): this;
56
- /** Removes one claim from the active entry. */
85
+ /** Removes one claim from this entry. */
57
86
  removeClaim(key: string): this;
87
+ /** Writes the staged entry `resource.id`. */
88
+ setResourceId(resourceId?: string | null): this;
89
+ /** Reads the staged entry `resource.id` when present. */
90
+ getResourceId(): string | undefined;
91
+ /** Writes the staged entry `fullUrl`. */
92
+ setFullUrl(fullUrl?: string | null): this;
93
+ /** Reads the staged entry `fullUrl` when present. */
94
+ getFullUrl(): string | undefined;
95
+ /** Returns control to the parent bundle editor. */
96
+ doneEntry(): BundleEditor;
97
+ protected getMutableEntry(): BuiltBundleEntry;
98
+ protected getClaims(): EmployeeClaims;
99
+ }
100
+ /**
101
+ * Employee-specific editor for one staged bundle entry.
102
+ *
103
+ * Use this class after `bundle.newEntry().asEmployee()` or
104
+ * `bundle.openEntry(...).asEmployee()`.
105
+ */
106
+ export declare class EmployeeEntryEditor extends BundleEntryEditor {
58
107
  /**
59
- * Sets the active entry identifier.
108
+ * Writes the canonical employee identifier.
60
109
  *
61
- * When informed, the value is synchronized into `fullUrl`, `resource.id`,
62
- * and the canonical identifier claim.
63
- * Empty values remove the identifier from all three places.
110
+ * The identifier is synchronized across:
111
+ * - `entry.fullUrl`
112
+ * - `resource.id`
113
+ * - `org.schema.Person.identifier`
64
114
  */
65
115
  setIdentifier(identifier?: string | null): this;
66
- /** Reads the active entry identifier from claims, resource id, or fullUrl. */
116
+ /** Reads the canonical employee identifier from claims, resource id, or fullUrl. */
67
117
  getIdentifier(): string | undefined;
68
- /** Ensures the active entry carries one canonical identifier. */
118
+ /** Ensures the employee entry carries one canonical `urn:uuid:*` identifier. */
69
119
  ensureIdentifier(): string;
70
- /** Sets the active entry `fullUrl` explicitly. */
71
- setFullUrl(fullUrl?: string | null): this;
72
- /** Returns the active entry `fullUrl` when present. */
73
- getFullUrl(): string | undefined;
74
- /** Convenience employee setter for email. */
120
+ /** Writes the canonical employee email claim on this entry. */
75
121
  setEmail(email: string): this;
76
- /** Convenience employee setter for occupational role. */
122
+ /** Reads the canonical employee email claim from this entry. */
123
+ getEmail(): string | undefined;
124
+ /** Writes the canonical employee occupational role claim on this entry. */
77
125
  setRole(role: string): this;
78
- /** Convenience employee setter for `worksFor`. */
126
+ /** Reads the canonical employee occupational role claim from this entry. */
127
+ getRole(): string | undefined;
128
+ /** Writes the canonical employee `worksFor` claim on this entry. */
79
129
  setWorksFor(worksFor: string): this;
80
- /** Convenience employee setter for `memberOf`. */
130
+ /** Writes the canonical employee `memberOf` claim on this entry. */
81
131
  setMemberOf(memberOf: string): this;
82
- /** Convenience employee setter for `memberOf.taxID`. */
132
+ /** Writes the canonical employee organization tax id claim on this entry. */
83
133
  setMemberOfOrgTaxId(taxId: string): this;
84
- private requireBundleOperation;
85
- private getRequiredActiveEntry;
86
- private getActiveEntryClaims;
87
- private getActiveOrSingleSearchClaims;
88
134
  }
@@ -1,7 +1,7 @@
1
1
  import { ClaimsPersonSchemaorg } from '../constants/schemaorg.js';
2
- import { EmployeeBatchEntryTypes, buildEmployeeBatchEntry, buildEmployeeClaims, buildEmployeePurgeBundle, buildEmployeeSearchBundle, EmployeeBundleMethods, EmployeeBundleOperations, } from './employee.js';
3
- function cloneEntry(entry) {
4
- return JSON.parse(JSON.stringify(entry));
2
+ import { buildEmployeeBatchEntry, buildEmployeePurgeBundle, buildEmployeeSearchBundle, EmployeeBatchEntryTypes, EmployeeBundleMethods, EmployeeBundleOperations, EmployeeResourceTypes, } from './employee.js';
3
+ function cloneEntry(value) {
4
+ return JSON.parse(JSON.stringify(value));
5
5
  }
6
6
  function cloneClaimValue(value) {
7
7
  if (Array.isArray(value)) {
@@ -16,20 +16,20 @@ function normalizeOptionalIdentifier(value) {
16
16
  const normalized = String(value).trim();
17
17
  return normalized ? normalized : undefined;
18
18
  }
19
- function createEmployeeIdentifierUrn() {
19
+ function createCanonicalIdentifierUrn() {
20
20
  const cryptoLike = globalThis;
21
21
  const uuid = typeof cryptoLike.crypto?.randomUUID === 'function'
22
22
  ? cryptoLike.crypto.randomUUID()
23
- : `employee-${Date.now()}-${Math.random().toString(16).slice(2)}`;
23
+ : `resource-${Date.now()}-${Math.random().toString(16).slice(2)}`;
24
24
  return `urn:uuid:${uuid}`;
25
25
  }
26
26
  function resolveRequestMethodForOperation(operation) {
27
27
  switch (operation) {
28
28
  case EmployeeBundleOperations.disable:
29
29
  return EmployeeBundleMethods.disable;
30
+ case EmployeeBundleOperations.search:
30
31
  case EmployeeBundleOperations.create:
31
32
  case EmployeeBundleOperations.purge:
32
- case EmployeeBundleOperations.search:
33
33
  default:
34
34
  return EmployeeBundleMethods.create;
35
35
  }
@@ -48,129 +48,221 @@ function resolveEntryTypeForOperation(operation) {
48
48
  }
49
49
  }
50
50
  /**
51
- * Generic bundle editor with an employee adapter surface.
51
+ * Generic bundle editor for FHIR-like bundle payloads assembled in memory.
52
52
  *
53
- * Current scope:
54
- * - one declared business operation per bundle
55
- * - active-entry editing through generic claim helpers
56
- * - employee convenience setters layered on the same active entry
53
+ * This class owns bundle-level concerns:
54
+ * - which business operation the bundle represents
55
+ * - which resource type the bundle allows
56
+ * - which entries belong to the bundle
57
+ * - when the final bundle is materialized through `build()`
57
58
  *
58
- * This editor lives in `common-utils` because it is runtime-neutral and can be
59
- * consumed by frontend SDKs, backend SDKs, and backend services.
59
+ * Resource-specific semantics belong to resource entry editors such as
60
+ * `EmployeeEntryEditor`.
60
61
  */
61
62
  export class BundleEditor {
62
63
  bundleOperation = null;
64
+ allowedResourceType = null;
63
65
  entries = [];
64
- activeEntryIndex = null;
65
- /** Declares which business operation the bundle is assembling. */
66
+ /** Declares which business operation this bundle is staging. */
66
67
  setBundleOperation(operation) {
67
68
  this.bundleOperation = operation;
68
69
  return this;
69
70
  }
70
- /** Returns the operation currently assigned to this bundle. */
71
+ /** Returns the current business operation assigned to the bundle. */
71
72
  getBundleOperation() {
72
73
  return this.bundleOperation;
73
74
  }
74
75
  /**
75
- * Opens one new active entry.
76
+ * Restricts the bundle to one resource type.
77
+ *
78
+ * For employee batch bundles this should be fixed to
79
+ * `EmployeeResourceTypes.employee` so the editor cannot mix unrelated
80
+ * resource kinds.
81
+ */
82
+ setAllowedResourceType(resourceType) {
83
+ this.allowedResourceType = resourceType;
84
+ return this;
85
+ }
86
+ /** Returns the bundle resource type restriction when already declared. */
87
+ getAllowedResourceType() {
88
+ return this.allowedResourceType;
89
+ }
90
+ /**
91
+ * Opens one new entry and returns a generic entry editor for that slot.
76
92
  *
77
- * If the current bundle operation needs an identifier and none is supplied,
78
- * a canonical `urn:uuid:*` identifier is generated and aligned across
79
- * `fullUrl`, `resource.id`, and `org.schema.Person.identifier`.
93
+ * The entry editor can later expose resource-specific semantics through
94
+ * methods such as `asEmployee()`.
80
95
  */
81
96
  newEntry(resourceId) {
82
97
  const operation = this.requireBundleOperation();
83
- const normalizedIdentifier = operation === EmployeeBundleOperations.search
84
- ? normalizeOptionalIdentifier(resourceId)
85
- : (normalizeOptionalIdentifier(resourceId) || createEmployeeIdentifierUrn());
86
- const claims = normalizedIdentifier
87
- ? buildEmployeeClaims({ identifier: normalizedIdentifier })
88
- : buildEmployeeClaims({});
89
- const entry = buildEmployeeBatchEntry({
90
- type: resolveEntryTypeForOperation(operation),
91
- method: resolveRequestMethodForOperation(operation),
92
- resourceId: normalizedIdentifier,
93
- claims,
94
- });
95
- if (normalizedIdentifier) {
96
- entry.fullUrl = normalizedIdentifier;
97
- }
98
+ const resourceType = this.requireAllowedResourceType();
99
+ const entry = this.createEntryDraft(operation, resourceType, resourceId);
98
100
  this.entries.push(entry);
99
- this.activeEntryIndex = this.entries.length - 1;
100
- return this;
101
+ return new BundleEntryEditor(this, this.entries.length - 1);
101
102
  }
102
- /** Reopens an existing entry by identifier or `fullUrl`. */
103
- openEntry(resourceId) {
104
- const normalizedIdentifier = normalizeOptionalIdentifier(resourceId);
103
+ /** Reopens one existing entry by `resource.id` or `fullUrl`. */
104
+ openEntry(resourceIdOrFullUrl) {
105
+ const normalizedIdentifier = normalizeOptionalIdentifier(resourceIdOrFullUrl);
105
106
  if (!normalizedIdentifier) {
106
- throw new Error('openEntry requires a non-empty resource identifier.');
107
+ throw new Error('openEntry requires a non-empty resource identifier or fullUrl.');
107
108
  }
108
- const nextIndex = this.entries.findIndex((entry) => {
109
- const claims = entry.resource?.meta?.claims || {};
109
+ const entryIndex = this.entries.findIndex((entry) => {
110
110
  return normalizeOptionalIdentifier(entry.resource?.id) === normalizedIdentifier
111
- || normalizeOptionalIdentifier(entry.fullUrl) === normalizedIdentifier
112
- || normalizeOptionalIdentifier(claims[ClaimsPersonSchemaorg.identifier]) === normalizedIdentifier;
111
+ || normalizeOptionalIdentifier(entry.fullUrl) === normalizedIdentifier;
113
112
  });
114
- if (nextIndex < 0) {
115
- throw new Error(`openEntry could not find resource identifier: ${normalizedIdentifier}`);
113
+ if (entryIndex < 0) {
114
+ throw new Error(`openEntry could not find resource identifier or fullUrl: ${normalizedIdentifier}`);
116
115
  }
117
- this.activeEntryIndex = nextIndex;
118
- return this;
116
+ return new BundleEntryEditor(this, entryIndex);
119
117
  }
120
- /** Closes the current active entry while preserving it inside the bundle draft. */
121
- doneEntry() {
122
- this.activeEntryIndex = null;
123
- return this;
124
- }
125
- /** Returns a cloned snapshot of the currently staged entries. */
118
+ /** Returns cloned staged entries for inspection or debugging. */
126
119
  getEntries() {
127
120
  return this.entries.map((entry) => cloneEntry(entry));
128
121
  }
129
122
  /**
130
- * Builds the final bundle payload for the declared operation.
123
+ * Materializes the final bundle payload from the editor state.
131
124
  *
132
- * `search` returns one canonical search bundle.
133
- * `purge` returns a batch bundle whose entries are routed later to
134
- * `Employee/_purge`.
135
- * `create` and `disable` return canonical batch bundles.
125
+ * `build()` does not send, sign, or wrap the payload. It only returns the
126
+ * final bundle object for the declared operation and staged entries.
136
127
  */
137
128
  build() {
138
129
  const operation = this.requireBundleOperation();
130
+ const resourceType = this.requireAllowedResourceType();
131
+ if (resourceType !== EmployeeResourceTypes.employee) {
132
+ throw new Error(`BundleEditor does not yet support build() for resource type: ${resourceType}`);
133
+ }
139
134
  if (operation === EmployeeBundleOperations.search) {
140
- const claims = this.getActiveOrSingleSearchClaims();
141
- return buildEmployeeSearchBundle({ claims });
135
+ return buildEmployeeSearchBundle({
136
+ claims: this.getSingleSearchClaims(),
137
+ resourceType,
138
+ });
142
139
  }
143
140
  if (operation === EmployeeBundleOperations.purge) {
144
141
  return {
145
- resourceType: 'Bundle',
146
- type: 'batch',
142
+ resourceType: EmployeeResourceTypes.bundle,
143
+ type: EmployeeResourceTypes.batch,
147
144
  entry: this.entries.map((entry) => {
148
- const identifier = normalizeOptionalIdentifier(entry.resource?.meta?.claims?.[ClaimsPersonSchemaorg.identifier]);
145
+ const identifier = normalizeOptionalIdentifier(entry.resource?.meta?.claims?.[ClaimsPersonSchemaorg.identifier]
146
+ || entry.resource?.id
147
+ || entry.fullUrl);
149
148
  if (!identifier) {
150
- throw new Error('Every purge entry requires org.schema.Person.identifier.');
149
+ throw new Error('Every purge entry requires one canonical employee identifier.');
151
150
  }
152
- return buildEmployeePurgeBundle({ identifier }).entry[0];
151
+ return buildEmployeePurgeBundle({
152
+ identifier,
153
+ resourceType,
154
+ }).entry[0];
153
155
  }),
154
156
  };
155
157
  }
156
158
  return {
157
- resourceType: 'Bundle',
158
- type: 'batch',
159
+ resourceType: EmployeeResourceTypes.bundle,
160
+ type: EmployeeResourceTypes.batch,
159
161
  entry: this.entries.map((entry) => cloneEntry(entry)),
160
162
  };
161
163
  }
162
- /** Reads one claim from the active entry. */
164
+ /** @internal */
165
+ getMutableEntry(entryIndex) {
166
+ if (!Number.isInteger(entryIndex) || entryIndex < 0 || entryIndex >= this.entries.length) {
167
+ throw new Error(`BundleEditor could not open entry index: ${entryIndex}`);
168
+ }
169
+ return this.entries[entryIndex];
170
+ }
171
+ createEntryDraft(operation, resourceType, resourceId) {
172
+ if (resourceType !== EmployeeResourceTypes.employee) {
173
+ throw new Error(`BundleEditor does not yet support newEntry() for resource type: ${resourceType}`);
174
+ }
175
+ const normalizedIdentifier = operation === EmployeeBundleOperations.search
176
+ ? normalizeOptionalIdentifier(resourceId)
177
+ : (normalizeOptionalIdentifier(resourceId) || createCanonicalIdentifierUrn());
178
+ const claims = {
179
+ '@context': 'org.schema',
180
+ ...(normalizedIdentifier ? { [ClaimsPersonSchemaorg.identifier]: normalizedIdentifier } : {}),
181
+ };
182
+ const entry = buildEmployeeBatchEntry({
183
+ type: resolveEntryTypeForOperation(operation),
184
+ method: resolveRequestMethodForOperation(operation),
185
+ resourceId: normalizedIdentifier,
186
+ resourceType,
187
+ claims,
188
+ });
189
+ if (normalizedIdentifier) {
190
+ entry.fullUrl = normalizedIdentifier;
191
+ }
192
+ return entry;
193
+ }
194
+ getSingleSearchClaims() {
195
+ if (this.entries.length === 0) {
196
+ return {};
197
+ }
198
+ if (this.entries.length > 1) {
199
+ throw new Error('Search bundles currently support one entry per bundle.');
200
+ }
201
+ const claims = this.entries[0].resource?.meta?.claims || {};
202
+ const searchClaims = {};
203
+ for (const [key, value] of Object.entries(claims)) {
204
+ if (value === undefined
205
+ || value === null
206
+ || typeof value === 'string'
207
+ || typeof value === 'number'
208
+ || typeof value === 'boolean') {
209
+ searchClaims[key] = value;
210
+ }
211
+ }
212
+ return searchClaims;
213
+ }
214
+ requireBundleOperation() {
215
+ if (!this.bundleOperation) {
216
+ throw new Error('BundleEditor requires setBundleOperation(...) before newEntry() or build().');
217
+ }
218
+ return this.bundleOperation;
219
+ }
220
+ requireAllowedResourceType() {
221
+ if (!this.allowedResourceType) {
222
+ throw new Error('BundleEditor requires setAllowedResourceType(...) before newEntry() or build().');
223
+ }
224
+ return this.allowedResourceType;
225
+ }
226
+ }
227
+ /**
228
+ * Generic editor for one staged bundle entry.
229
+ *
230
+ * This class only knows generic entry concerns such as:
231
+ * - resource id
232
+ * - fullUrl
233
+ * - meta claims
234
+ * - conversion to one resource-specific entry editor
235
+ */
236
+ export class BundleEntryEditor {
237
+ bundleEditor;
238
+ entryIndex;
239
+ constructor(bundleEditor, entryIndex) {
240
+ this.bundleEditor = bundleEditor;
241
+ this.entryIndex = entryIndex;
242
+ }
243
+ /** Opens the current entry as one employee-specific resource editor. */
244
+ asEmployee() {
245
+ const entry = this.getMutableEntry();
246
+ if (entry.resource?.resourceType !== EmployeeResourceTypes.employee) {
247
+ throw new Error(`BundleEntryEditor cannot open this entry as Employee: ${String(entry.resource?.resourceType || '')}`);
248
+ }
249
+ return new EmployeeEntryEditor(this.bundleEditor, this.entryIndex);
250
+ }
251
+ /** Reads one claim from this entry. */
163
252
  getClaim(key) {
164
- return cloneClaimValue(this.getActiveEntryClaims()[String(key).trim()]);
253
+ return cloneClaimValue(this.getClaims()[String(key).trim()]);
165
254
  }
166
- /** Checks whether the active entry contains one claim key. */
255
+ /** Checks whether this entry contains one claim key. */
167
256
  hasClaim(key) {
168
- return Object.prototype.hasOwnProperty.call(this.getActiveEntryClaims(), String(key).trim());
257
+ return Object.prototype.hasOwnProperty.call(this.getClaims(), String(key).trim());
169
258
  }
170
- /** Writes one claim on the active entry. */
259
+ /** Writes one claim on this entry. */
171
260
  setClaim(key, value) {
172
- const entry = this.getRequiredActiveEntry();
173
- entry.resource = entry.resource || { resourceType: 'Employee', meta: { claims: {} } };
261
+ const entry = this.getMutableEntry();
262
+ entry.resource = entry.resource || {
263
+ resourceType: this.bundleEditor.getAllowedResourceType() || EmployeeResourceTypes.employee,
264
+ meta: { claims: {} },
265
+ };
174
266
  entry.resource.meta = entry.resource.meta || {};
175
267
  entry.resource.meta.claims = {
176
268
  ...(entry.resource.meta.claims || {}),
@@ -178,7 +270,7 @@ export class BundleEditor {
178
270
  };
179
271
  return this;
180
272
  }
181
- /** Appends one claim value on the active entry. */
273
+ /** Appends one claim value on this entry. */
182
274
  addClaim(key, value) {
183
275
  const normalizedKey = String(key).trim();
184
276
  const current = this.getClaim(normalizedKey);
@@ -190,127 +282,140 @@ export class BundleEditor {
190
282
  }
191
283
  return this.setClaim(normalizedKey, [current, cloneClaimValue(value)]);
192
284
  }
193
- /** Removes one claim from the active entry. */
285
+ /** Removes one claim from this entry. */
194
286
  removeClaim(key) {
195
- const entry = this.getRequiredActiveEntry();
287
+ const entry = this.getMutableEntry();
196
288
  const claims = {
197
289
  ...(entry.resource?.meta?.claims || {}),
198
290
  };
199
291
  delete claims[String(key).trim()];
200
- entry.resource = entry.resource || { resourceType: 'Employee', meta: { claims: {} } };
292
+ entry.resource = entry.resource || {
293
+ resourceType: this.bundleEditor.getAllowedResourceType() || EmployeeResourceTypes.employee,
294
+ meta: { claims: {} },
295
+ };
201
296
  entry.resource.meta = entry.resource.meta || {};
202
297
  entry.resource.meta.claims = claims;
203
298
  return this;
204
299
  }
300
+ /** Writes the staged entry `resource.id`. */
301
+ setResourceId(resourceId) {
302
+ const entry = this.getMutableEntry();
303
+ const normalized = normalizeOptionalIdentifier(resourceId);
304
+ if (!normalized) {
305
+ delete entry.resource?.id;
306
+ return this;
307
+ }
308
+ entry.resource = entry.resource || {
309
+ resourceType: this.bundleEditor.getAllowedResourceType() || EmployeeResourceTypes.employee,
310
+ meta: { claims: {} },
311
+ };
312
+ entry.resource.id = normalized;
313
+ return this;
314
+ }
315
+ /** Reads the staged entry `resource.id` when present. */
316
+ getResourceId() {
317
+ return normalizeOptionalIdentifier(this.getMutableEntry().resource?.id);
318
+ }
319
+ /** Writes the staged entry `fullUrl`. */
320
+ setFullUrl(fullUrl) {
321
+ const entry = this.getMutableEntry();
322
+ const normalized = normalizeOptionalIdentifier(fullUrl);
323
+ if (!normalized) {
324
+ delete entry.fullUrl;
325
+ return this;
326
+ }
327
+ entry.fullUrl = normalized;
328
+ return this;
329
+ }
330
+ /** Reads the staged entry `fullUrl` when present. */
331
+ getFullUrl() {
332
+ return normalizeOptionalIdentifier(this.getMutableEntry().fullUrl);
333
+ }
334
+ /** Returns control to the parent bundle editor. */
335
+ doneEntry() {
336
+ return this.bundleEditor;
337
+ }
338
+ getMutableEntry() {
339
+ return this.bundleEditor.getMutableEntry(this.entryIndex);
340
+ }
341
+ getClaims() {
342
+ return {
343
+ ...(this.getMutableEntry().resource?.meta?.claims || {}),
344
+ };
345
+ }
346
+ }
347
+ /**
348
+ * Employee-specific editor for one staged bundle entry.
349
+ *
350
+ * Use this class after `bundle.newEntry().asEmployee()` or
351
+ * `bundle.openEntry(...).asEmployee()`.
352
+ */
353
+ export class EmployeeEntryEditor extends BundleEntryEditor {
205
354
  /**
206
- * Sets the active entry identifier.
355
+ * Writes the canonical employee identifier.
207
356
  *
208
- * When informed, the value is synchronized into `fullUrl`, `resource.id`,
209
- * and the canonical identifier claim.
210
- * Empty values remove the identifier from all three places.
357
+ * The identifier is synchronized across:
358
+ * - `entry.fullUrl`
359
+ * - `resource.id`
360
+ * - `org.schema.Person.identifier`
211
361
  */
212
362
  setIdentifier(identifier) {
213
363
  const normalized = normalizeOptionalIdentifier(identifier);
214
- const entry = this.getRequiredActiveEntry();
215
364
  if (!normalized) {
216
365
  this.removeClaim(ClaimsPersonSchemaorg.identifier);
217
- delete entry.resource?.id;
218
- delete entry.fullUrl;
366
+ this.setResourceId(undefined);
367
+ this.setFullUrl(undefined);
219
368
  return this;
220
369
  }
221
370
  this.setClaim(ClaimsPersonSchemaorg.identifier, normalized);
222
- entry.resource = entry.resource || { resourceType: 'Employee', meta: { claims: {} } };
223
- entry.resource.id = normalized;
224
- entry.fullUrl = normalized;
371
+ this.setResourceId(normalized);
372
+ this.setFullUrl(normalized);
225
373
  return this;
226
374
  }
227
- /** Reads the active entry identifier from claims, resource id, or fullUrl. */
375
+ /** Reads the canonical employee identifier from claims, resource id, or fullUrl. */
228
376
  getIdentifier() {
229
- const entry = this.getRequiredActiveEntry();
230
- return normalizeOptionalIdentifier(entry.resource?.meta?.claims?.[ClaimsPersonSchemaorg.identifier]
231
- || entry.resource?.id
232
- || entry.fullUrl);
377
+ return normalizeOptionalIdentifier(this.getClaim(ClaimsPersonSchemaorg.identifier)
378
+ || this.getResourceId()
379
+ || this.getFullUrl());
233
380
  }
234
- /** Ensures the active entry carries one canonical identifier. */
381
+ /** Ensures the employee entry carries one canonical `urn:uuid:*` identifier. */
235
382
  ensureIdentifier() {
236
383
  const existing = this.getIdentifier();
237
384
  if (existing) {
238
385
  return existing;
239
386
  }
240
- const generated = createEmployeeIdentifierUrn();
387
+ const generated = createCanonicalIdentifierUrn();
241
388
  this.setIdentifier(generated);
242
389
  return generated;
243
390
  }
244
- /** Sets the active entry `fullUrl` explicitly. */
245
- setFullUrl(fullUrl) {
246
- const entry = this.getRequiredActiveEntry();
247
- const normalized = normalizeOptionalIdentifier(fullUrl);
248
- if (!normalized) {
249
- delete entry.fullUrl;
250
- return this;
251
- }
252
- entry.fullUrl = normalized;
253
- return this;
254
- }
255
- /** Returns the active entry `fullUrl` when present. */
256
- getFullUrl() {
257
- return normalizeOptionalIdentifier(this.getRequiredActiveEntry().fullUrl);
258
- }
259
- /** Convenience employee setter for email. */
391
+ /** Writes the canonical employee email claim on this entry. */
260
392
  setEmail(email) {
261
393
  return this.setClaim(ClaimsPersonSchemaorg.email, String(email).trim());
262
394
  }
263
- /** Convenience employee setter for occupational role. */
395
+ /** Reads the canonical employee email claim from this entry. */
396
+ getEmail() {
397
+ const email = this.getClaim(ClaimsPersonSchemaorg.email);
398
+ return typeof email === 'string' && email.trim() ? email.trim() : undefined;
399
+ }
400
+ /** Writes the canonical employee occupational role claim on this entry. */
264
401
  setRole(role) {
265
402
  return this.setClaim(ClaimsPersonSchemaorg.hasOccupationalRoleValue, String(role).trim());
266
403
  }
267
- /** Convenience employee setter for `worksFor`. */
404
+ /** Reads the canonical employee occupational role claim from this entry. */
405
+ getRole() {
406
+ const role = this.getClaim(ClaimsPersonSchemaorg.hasOccupationalRoleValue);
407
+ return typeof role === 'string' && role.trim() ? role.trim() : undefined;
408
+ }
409
+ /** Writes the canonical employee `worksFor` claim on this entry. */
268
410
  setWorksFor(worksFor) {
269
411
  return this.setClaim(ClaimsPersonSchemaorg.worksFor, String(worksFor).trim());
270
412
  }
271
- /** Convenience employee setter for `memberOf`. */
413
+ /** Writes the canonical employee `memberOf` claim on this entry. */
272
414
  setMemberOf(memberOf) {
273
415
  return this.setClaim(ClaimsPersonSchemaorg.memberOf, String(memberOf).trim());
274
416
  }
275
- /** Convenience employee setter for `memberOf.taxID`. */
417
+ /** Writes the canonical employee organization tax id claim on this entry. */
276
418
  setMemberOfOrgTaxId(taxId) {
277
419
  return this.setClaim(ClaimsPersonSchemaorg.memberOfOrgTaxId, String(taxId).trim());
278
420
  }
279
- requireBundleOperation() {
280
- if (!this.bundleOperation) {
281
- throw new Error('BundleEditor requires setBundleOperation(...) before newEntry() or build().');
282
- }
283
- return this.bundleOperation;
284
- }
285
- getRequiredActiveEntry() {
286
- if (this.activeEntryIndex === null) {
287
- throw new Error('BundleEditor requires one active entry. Call newEntry(...) or openEntry(...) first.');
288
- }
289
- return this.entries[this.activeEntryIndex];
290
- }
291
- getActiveEntryClaims() {
292
- return {
293
- ...(this.getRequiredActiveEntry().resource?.meta?.claims || {}),
294
- };
295
- }
296
- getActiveOrSingleSearchClaims() {
297
- if (this.entries.length === 0) {
298
- return {};
299
- }
300
- if (this.entries.length > 1) {
301
- throw new Error('Search bundles currently support one search entry per bundle.');
302
- }
303
- const claims = this.entries[0].resource?.meta?.claims || {};
304
- const searchClaims = {};
305
- for (const [key, value] of Object.entries(claims)) {
306
- if (value === undefined
307
- || value === null
308
- || typeof value === 'string'
309
- || typeof value === 'number'
310
- || typeof value === 'boolean') {
311
- searchClaims[key] = value;
312
- }
313
- }
314
- return searchClaims;
315
- }
316
421
  }
@@ -0,0 +1,34 @@
1
+ export type BundleReaderEntry = Record<string, unknown>;
2
+ /**
3
+ * Runtime-neutral reader for built or received FHIR-like bundles.
4
+ *
5
+ * The reader does not assume one business resource. It only exposes generic
6
+ * bundle navigation, per-entry response inspection, and aggregate counters.
7
+ */
8
+ export declare class BundleReader {
9
+ private readonly bundle;
10
+ private activeEntryIndex;
11
+ constructor(bundle: Record<string, unknown>);
12
+ /** Returns the current bundle resource type when present. */
13
+ getResourceType(): string | undefined;
14
+ /** Returns the current FHIR bundle type when present. */
15
+ getBundleType(): string | undefined;
16
+ /** Returns cloned bundle entries. */
17
+ getEntries(): readonly BundleReaderEntry[];
18
+ /** Opens one entry by index. */
19
+ openEntry(index: number): this;
20
+ /** Returns the active entry response status when present. */
21
+ getEntryResponseStatus(): string | undefined;
22
+ /** Returns all active entry issue severities. */
23
+ getIssueSeverities(): string[];
24
+ /** Returns all active entry issue diagnostics strings. */
25
+ getIssueDiagnostics(): string[];
26
+ /** Returns the number of bundle entries. */
27
+ getTotalOperations(): number;
28
+ /** Returns the number of entries with one 2xx response status. */
29
+ getTotalSuccessfulOperations(): number;
30
+ /** Returns the number of entries without one 2xx response status. */
31
+ getTotalErrorOperations(): number;
32
+ private getRequiredActiveEntry;
33
+ private getActiveEntryIssues;
34
+ }
@@ -0,0 +1,88 @@
1
+ function cloneEntry(value) {
2
+ return JSON.parse(JSON.stringify(value));
3
+ }
4
+ /**
5
+ * Runtime-neutral reader for built or received FHIR-like bundles.
6
+ *
7
+ * The reader does not assume one business resource. It only exposes generic
8
+ * bundle navigation, per-entry response inspection, and aggregate counters.
9
+ */
10
+ export class BundleReader {
11
+ bundle;
12
+ activeEntryIndex = null;
13
+ constructor(bundle) {
14
+ this.bundle = cloneEntry(bundle);
15
+ }
16
+ /** Returns the current bundle resource type when present. */
17
+ getResourceType() {
18
+ const resourceType = this.bundle.resourceType;
19
+ return typeof resourceType === 'string' && resourceType.trim() ? resourceType.trim() : undefined;
20
+ }
21
+ /** Returns the current FHIR bundle type when present. */
22
+ getBundleType() {
23
+ const bundleType = this.bundle.type;
24
+ return typeof bundleType === 'string' && bundleType.trim() ? bundleType.trim() : undefined;
25
+ }
26
+ /** Returns cloned bundle entries. */
27
+ getEntries() {
28
+ const entries = this.bundle.entry;
29
+ return Array.isArray(entries) ? entries.map((entry) => cloneEntry(entry)) : [];
30
+ }
31
+ /** Opens one entry by index. */
32
+ openEntry(index) {
33
+ const entries = this.getEntries();
34
+ if (!Number.isInteger(index) || index < 0 || index >= entries.length) {
35
+ throw new Error(`BundleReader could not open entry index: ${index}`);
36
+ }
37
+ this.activeEntryIndex = index;
38
+ return this;
39
+ }
40
+ /** Returns the active entry response status when present. */
41
+ getEntryResponseStatus() {
42
+ const entry = this.getRequiredActiveEntry();
43
+ const response = entry.response;
44
+ const status = response?.status;
45
+ return typeof status === 'string' && status.trim() ? status.trim() : undefined;
46
+ }
47
+ /** Returns all active entry issue severities. */
48
+ getIssueSeverities() {
49
+ return this.getActiveEntryIssues()
50
+ .map((issue) => issue.severity)
51
+ .filter((severity) => typeof severity === 'string' && severity.trim().length > 0);
52
+ }
53
+ /** Returns all active entry issue diagnostics strings. */
54
+ getIssueDiagnostics() {
55
+ return this.getActiveEntryIssues()
56
+ .map((issue) => issue.diagnostics)
57
+ .filter((diagnostics) => typeof diagnostics === 'string' && diagnostics.trim().length > 0);
58
+ }
59
+ /** Returns the number of bundle entries. */
60
+ getTotalOperations() {
61
+ return this.getEntries().length;
62
+ }
63
+ /** Returns the number of entries with one 2xx response status. */
64
+ getTotalSuccessfulOperations() {
65
+ return this.getEntries().filter((entry) => {
66
+ const response = entry.response;
67
+ return typeof response?.status === 'string' && /^2\d\d$/.test(response.status);
68
+ }).length;
69
+ }
70
+ /** Returns the number of entries without one 2xx response status. */
71
+ getTotalErrorOperations() {
72
+ return this.getTotalOperations() - this.getTotalSuccessfulOperations();
73
+ }
74
+ getRequiredActiveEntry() {
75
+ if (this.activeEntryIndex === null) {
76
+ throw new Error('BundleReader requires one active entry. Call openEntry(...) first.');
77
+ }
78
+ const entries = this.getEntries();
79
+ return entries[this.activeEntryIndex];
80
+ }
81
+ getActiveEntryIssues() {
82
+ const entry = this.getRequiredActiveEntry();
83
+ const response = entry.response;
84
+ const outcome = response?.outcome;
85
+ const issue = outcome?.issue;
86
+ return Array.isArray(issue) ? issue.filter((value) => !!value && typeof value === 'object') : [];
87
+ }
88
+ }
@@ -0,0 +1,24 @@
1
+ import { BundleEditor } from './bundle-editor';
2
+ /**
3
+ * High-level employee adapter on top of the shared `BundleEditor`.
4
+ *
5
+ * Use this class when the active bundle entry is known to represent one
6
+ * employee resource and callers should work with employee semantics instead of
7
+ * raw claim keys.
8
+ */
9
+ export declare class EmployeeEditor extends BundleEditor {
10
+ /** Writes the canonical employee email claim on the active entry. */
11
+ setEmail(email: string): this;
12
+ /** Reads the canonical employee email claim from the active entry. */
13
+ getEmail(): string | undefined;
14
+ /** Writes the canonical employee occupational role claim on the active entry. */
15
+ setRole(role: string): this;
16
+ /** Reads the canonical employee occupational role claim from the active entry. */
17
+ getRole(): string | undefined;
18
+ /** Writes the canonical employee `worksFor` claim on the active entry. */
19
+ setWorksFor(worksFor: string): this;
20
+ /** Writes the canonical employee `memberOf` claim on the active entry. */
21
+ setMemberOf(memberOf: string): this;
22
+ /** Writes the canonical employee organization tax id claim on the active entry. */
23
+ setMemberOfOrgTaxId(taxId: string): this;
24
+ }
@@ -0,0 +1,41 @@
1
+ import { ClaimsPersonSchemaorg } from '../constants/schemaorg.js';
2
+ import { BundleEditor } from './bundle-editor.js';
3
+ /**
4
+ * High-level employee adapter on top of the shared `BundleEditor`.
5
+ *
6
+ * Use this class when the active bundle entry is known to represent one
7
+ * employee resource and callers should work with employee semantics instead of
8
+ * raw claim keys.
9
+ */
10
+ export class EmployeeEditor extends BundleEditor {
11
+ /** Writes the canonical employee email claim on the active entry. */
12
+ setEmail(email) {
13
+ return this.setClaim(ClaimsPersonSchemaorg.email, String(email).trim());
14
+ }
15
+ /** Reads the canonical employee email claim from the active entry. */
16
+ getEmail() {
17
+ const email = this.getClaim(ClaimsPersonSchemaorg.email);
18
+ return typeof email === 'string' && email.trim() ? email.trim() : undefined;
19
+ }
20
+ /** Writes the canonical employee occupational role claim on the active entry. */
21
+ setRole(role) {
22
+ return this.setClaim(ClaimsPersonSchemaorg.hasOccupationalRoleValue, String(role).trim());
23
+ }
24
+ /** Reads the canonical employee occupational role claim from the active entry. */
25
+ getRole() {
26
+ const role = this.getClaim(ClaimsPersonSchemaorg.hasOccupationalRoleValue);
27
+ return typeof role === 'string' && role.trim() ? role.trim() : undefined;
28
+ }
29
+ /** Writes the canonical employee `worksFor` claim on the active entry. */
30
+ setWorksFor(worksFor) {
31
+ return this.setClaim(ClaimsPersonSchemaorg.worksFor, String(worksFor).trim());
32
+ }
33
+ /** Writes the canonical employee `memberOf` claim on the active entry. */
34
+ setMemberOf(memberOf) {
35
+ return this.setClaim(ClaimsPersonSchemaorg.memberOf, String(memberOf).trim());
36
+ }
37
+ /** Writes the canonical employee organization tax id claim on the active entry. */
38
+ setMemberOfOrgTaxId(taxId) {
39
+ return this.setClaim(ClaimsPersonSchemaorg.memberOfOrgTaxId, String(taxId).trim());
40
+ }
41
+ }
@@ -48,6 +48,12 @@ export declare const EmployeeBundleMethods: Readonly<{
48
48
  export declare const EmployeeBundleRoutes: Readonly<{
49
49
  readonly search: "Employee/_search";
50
50
  }>;
51
+ export declare const EmployeeResourceTypes: Readonly<{
52
+ readonly employee: "Employee";
53
+ readonly bundle: "Bundle";
54
+ readonly batch: "batch";
55
+ readonly parameters: "Parameters";
56
+ }>;
51
57
  export declare const EmployeeBatchEntryTypes: Readonly<{
52
58
  readonly create: "Employee-create-request-v1.0";
53
59
  readonly disable: "Employee-disable-request-v1.0";
@@ -15,6 +15,12 @@ export const EmployeeBundleMethods = Object.freeze({
15
15
  export const EmployeeBundleRoutes = Object.freeze({
16
16
  search: 'Employee/_search',
17
17
  });
18
+ export const EmployeeResourceTypes = Object.freeze({
19
+ employee: 'Employee',
20
+ bundle: 'Bundle',
21
+ batch: 'batch',
22
+ parameters: 'Parameters',
23
+ });
18
24
  export const EmployeeBatchEntryTypes = Object.freeze({
19
25
  create: 'Employee-create-request-v1.0',
20
26
  disable: 'Employee-disable-request-v1.0',
@@ -4,6 +4,7 @@ export * from './base-convert';
4
4
  export * from './baseN';
5
5
  export * from './bundle';
6
6
  export * from './bundle-editor';
7
+ export * from './bundle-reader';
7
8
  export * from './bundle-query';
8
9
  export * from '../claims';
9
10
  export * from './content';
@@ -4,6 +4,7 @@ export * from './base-convert.js';
4
4
  export * from './baseN.js';
5
5
  export * from './bundle.js';
6
6
  export * from './bundle-editor.js';
7
+ export * from './bundle-reader.js';
7
8
  export * from './bundle-query.js';
8
9
  export * from '../claims/index.js';
9
10
  export * from './content.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdc-common-utils-ts",
3
- "version": "1.15.0",
3
+ "version": "1.16.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },