@api-client/core 0.19.8 → 0.19.10

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 (106) hide show
  1. package/Testing.md +1 -1
  2. package/build/src/decorators/observed.d.ts.map +1 -1
  3. package/build/src/decorators/observed.js +91 -0
  4. package/build/src/decorators/observed.js.map +1 -1
  5. package/build/src/modeling/ApiModel.d.ts +21 -7
  6. package/build/src/modeling/ApiModel.d.ts.map +1 -1
  7. package/build/src/modeling/ApiModel.js +70 -29
  8. package/build/src/modeling/ApiModel.js.map +1 -1
  9. package/build/src/modeling/DomainValidation.d.ts +1 -1
  10. package/build/src/modeling/DomainValidation.d.ts.map +1 -1
  11. package/build/src/modeling/DomainValidation.js.map +1 -1
  12. package/build/src/modeling/ExposedEntity.d.ts +14 -0
  13. package/build/src/modeling/ExposedEntity.d.ts.map +1 -1
  14. package/build/src/modeling/ExposedEntity.js +59 -6
  15. package/build/src/modeling/ExposedEntity.js.map +1 -1
  16. package/build/src/modeling/actions/Action.d.ts +11 -1
  17. package/build/src/modeling/actions/Action.d.ts.map +1 -1
  18. package/build/src/modeling/actions/Action.js +21 -3
  19. package/build/src/modeling/actions/Action.js.map +1 -1
  20. package/build/src/modeling/actions/CreateAction.d.ts +2 -1
  21. package/build/src/modeling/actions/CreateAction.d.ts.map +1 -1
  22. package/build/src/modeling/actions/CreateAction.js +2 -2
  23. package/build/src/modeling/actions/CreateAction.js.map +1 -1
  24. package/build/src/modeling/actions/DeleteAction.d.ts +2 -1
  25. package/build/src/modeling/actions/DeleteAction.d.ts.map +1 -1
  26. package/build/src/modeling/actions/DeleteAction.js +2 -2
  27. package/build/src/modeling/actions/DeleteAction.js.map +1 -1
  28. package/build/src/modeling/actions/ListAction.d.ts +2 -1
  29. package/build/src/modeling/actions/ListAction.d.ts.map +1 -1
  30. package/build/src/modeling/actions/ListAction.js +2 -2
  31. package/build/src/modeling/actions/ListAction.js.map +1 -1
  32. package/build/src/modeling/actions/ReadAction.d.ts +2 -1
  33. package/build/src/modeling/actions/ReadAction.d.ts.map +1 -1
  34. package/build/src/modeling/actions/ReadAction.js +2 -2
  35. package/build/src/modeling/actions/ReadAction.js.map +1 -1
  36. package/build/src/modeling/actions/SearchAction.d.ts +2 -1
  37. package/build/src/modeling/actions/SearchAction.d.ts.map +1 -1
  38. package/build/src/modeling/actions/SearchAction.js +2 -2
  39. package/build/src/modeling/actions/SearchAction.js.map +1 -1
  40. package/build/src/modeling/actions/UpdateAction.d.ts +2 -1
  41. package/build/src/modeling/actions/UpdateAction.d.ts.map +1 -1
  42. package/build/src/modeling/actions/UpdateAction.js +2 -2
  43. package/build/src/modeling/actions/UpdateAction.js.map +1 -1
  44. package/build/src/modeling/actions/index.d.ts +2 -1
  45. package/build/src/modeling/actions/index.d.ts.map +1 -1
  46. package/build/src/modeling/actions/index.js +7 -7
  47. package/build/src/modeling/actions/index.js.map +1 -1
  48. package/build/src/modeling/ai/types.d.ts +4 -4
  49. package/build/src/modeling/ai/types.d.ts.map +1 -1
  50. package/build/src/modeling/ai/types.js.map +1 -1
  51. package/build/src/modeling/index.d.ts +1 -0
  52. package/build/src/modeling/index.d.ts.map +1 -1
  53. package/build/src/modeling/index.js +1 -0
  54. package/build/src/modeling/index.js.map +1 -1
  55. package/build/src/modeling/types.d.ts +67 -0
  56. package/build/src/modeling/types.d.ts.map +1 -1
  57. package/build/src/modeling/types.js.map +1 -1
  58. package/build/src/modeling/validation/api_model_rules.d.ts +15 -0
  59. package/build/src/modeling/validation/api_model_rules.d.ts.map +1 -0
  60. package/build/src/modeling/validation/api_model_rules.js +599 -0
  61. package/build/src/modeling/validation/api_model_rules.js.map +1 -0
  62. package/build/src/modeling/validation/association_validation.d.ts.map +1 -1
  63. package/build/src/modeling/validation/association_validation.js +1 -3
  64. package/build/src/modeling/validation/association_validation.js.map +1 -1
  65. package/build/src/sdk/AiSdk.d.ts.map +1 -1
  66. package/build/src/sdk/AiSdk.js +2 -1
  67. package/build/src/sdk/AiSdk.js.map +1 -1
  68. package/build/tsconfig.tsbuildinfo +1 -1
  69. package/data/models/example-generator-api.json +12 -12
  70. package/eslint.config.js +0 -1
  71. package/package.json +17 -122
  72. package/src/decorators/observed.ts +91 -0
  73. package/src/modeling/ApiModel.ts +73 -33
  74. package/src/modeling/DomainValidation.ts +1 -1
  75. package/src/modeling/ExposedEntity.ts +63 -9
  76. package/src/modeling/actions/Action.ts +25 -2
  77. package/src/modeling/actions/CreateAction.ts +3 -2
  78. package/src/modeling/actions/DeleteAction.ts +3 -2
  79. package/src/modeling/actions/ListAction.ts +3 -2
  80. package/src/modeling/actions/ReadAction.ts +3 -2
  81. package/src/modeling/actions/SearchAction.ts +3 -2
  82. package/src/modeling/actions/UpdateAction.ts +3 -2
  83. package/src/modeling/ai/types.ts +4 -4
  84. package/src/modeling/types.ts +70 -0
  85. package/src/modeling/validation/api_model_rules.ts +640 -0
  86. package/src/modeling/validation/api_model_validation_rules.md +58 -0
  87. package/src/modeling/validation/association_validation.ts +1 -3
  88. package/src/sdk/AiSdk.ts +5 -4
  89. package/tests/unit/modeling/actions/Action.spec.ts +40 -8
  90. package/tests/unit/modeling/actions/CreateAction.spec.ts +5 -5
  91. package/tests/unit/modeling/actions/DeleteAction.spec.ts +6 -6
  92. package/tests/unit/modeling/actions/ListAction.spec.ts +7 -7
  93. package/tests/unit/modeling/actions/ReadAction.spec.ts +6 -6
  94. package/tests/unit/modeling/actions/SearchAction.spec.ts +6 -6
  95. package/tests/unit/modeling/actions/UpdateAction.spec.ts +6 -6
  96. package/tests/unit/modeling/api_model.spec.ts +190 -13
  97. package/tests/unit/modeling/api_model_expose_entity.spec.ts +43 -19
  98. package/tests/unit/modeling/api_model_remove_entity.spec.ts +6 -6
  99. package/tests/unit/modeling/exposed_entity.spec.ts +123 -3
  100. package/tests/unit/modeling/exposed_entity_actions.spec.ts +41 -18
  101. package/tests/unit/modeling/exposed_entity_setter_validation.spec.ts +1 -1
  102. package/tests/unit/modeling/rules/restoring_rules.spec.ts +9 -5
  103. package/tests/unit/modeling/validation/api_model_rules.spec.ts +324 -0
  104. package/tsconfig.browser.json +1 -1
  105. package/tsconfig.node.json +1 -1
  106. package/bin/test-web.ts +0 -6
@@ -42810,16 +42810,16 @@
42810
42810
  "@id": "#219"
42811
42811
  },
42812
42812
  {
42813
- "@id": "#219"
42813
+ "@id": "#213"
42814
42814
  },
42815
42815
  {
42816
- "@id": "#216"
42816
+ "@id": "#210"
42817
42817
  },
42818
42818
  {
42819
- "@id": "#210"
42819
+ "@id": "#216"
42820
42820
  },
42821
42821
  {
42822
- "@id": "#213"
42822
+ "@id": "#219"
42823
42823
  }
42824
42824
  ],
42825
42825
  "doc:root": false,
@@ -44232,7 +44232,7 @@
44232
44232
  "doc:ExternalDomainElement",
44233
44233
  "doc:DomainElement"
44234
44234
  ],
44235
- "doc:raw": "-\n type: 'GENERAL'\n value: 'info@company.be'\n-\n type: 'IT_DEPT'\n value: 'it-service@company.be'\n",
44235
+ "doc:raw": "type: 'GENERAL'\ncountryDialCode : '+32'\nareaCode : '21'\nsubscriberNumber: '12.87.00'\nformatted: '+32-(0)21 302099'\n",
44236
44236
  "core:mediaType": "application/yaml",
44237
44237
  "sourcemaps:sources": [
44238
44238
  {
@@ -44253,7 +44253,7 @@
44253
44253
  "doc:ExternalDomainElement",
44254
44254
  "doc:DomainElement"
44255
44255
  ],
44256
- "doc:raw": "type: \"GENERAL\"\nvalue: \"www.company.be\"\n",
44256
+ "doc:raw": "type: 'GENERAL'\ncountryDialCode : '+32'\nareaCode : '22'\nsubscriberNumber: '12.87.00'\nformatted: '+32-(0)22 000000'\n",
44257
44257
  "core:mediaType": "application/yaml",
44258
44258
  "sourcemaps:sources": [
44259
44259
  {
@@ -44274,7 +44274,7 @@
44274
44274
  "doc:ExternalDomainElement",
44275
44275
  "doc:DomainElement"
44276
44276
  ],
44277
- "doc:raw": "type: 'GENERAL'\ncountryDialCode : '+32'\nareaCode : '21'\nsubscriberNumber: '12.87.00'\nformatted: '+32-(0)21 302099'\n",
44277
+ "doc:raw": "-\n type: 'GENERAL'\n value: 'info@company.be'\n-\n type: 'IT_DEPT'\n value: 'it-service@company.be'\n",
44278
44278
  "core:mediaType": "application/yaml",
44279
44279
  "sourcemaps:sources": [
44280
44280
  {
@@ -44295,7 +44295,7 @@
44295
44295
  "doc:ExternalDomainElement",
44296
44296
  "doc:DomainElement"
44297
44297
  ],
44298
- "doc:raw": "type: 'GENERAL'\ncountryDialCode : '+32'\nareaCode : '22'\nsubscriberNumber: '12.87.00'\nformatted: '+32-(0)22 000000'\n",
44298
+ "doc:raw": "type: \"GENERAL\"\nvalue: \"www.company.be\"\n",
44299
44299
  "core:mediaType": "application/yaml",
44300
44300
  "sourcemaps:sources": [
44301
44301
  {
@@ -45116,22 +45116,22 @@
45116
45116
  {
45117
45117
  "@id": "#212/source-map/lexical/element_0",
45118
45118
  "sourcemaps:element": "amf://id#212",
45119
- "sourcemaps:value": "[(1,0)-(7,0)]"
45119
+ "sourcemaps:value": "[(1,0)-(6,0)]"
45120
45120
  },
45121
45121
  {
45122
45122
  "@id": "#215/source-map/lexical/element_0",
45123
45123
  "sourcemaps:element": "amf://id#215",
45124
- "sourcemaps:value": "[(1,0)-(3,0)]"
45124
+ "sourcemaps:value": "[(1,0)-(6,0)]"
45125
45125
  },
45126
45126
  {
45127
45127
  "@id": "#218/source-map/lexical/element_0",
45128
45128
  "sourcemaps:element": "amf://id#218",
45129
- "sourcemaps:value": "[(1,0)-(6,0)]"
45129
+ "sourcemaps:value": "[(1,0)-(7,0)]"
45130
45130
  },
45131
45131
  {
45132
45132
  "@id": "#221/source-map/lexical/element_0",
45133
45133
  "sourcemaps:element": "amf://id#221",
45134
- "sourcemaps:value": "[(1,0)-(6,0)]"
45134
+ "sourcemaps:value": "[(1,0)-(3,0)]"
45135
45135
  },
45136
45136
  {
45137
45137
  "@id": "#338/source-map/synthesized-field/element_1",
package/eslint.config.js CHANGED
@@ -16,7 +16,6 @@ export const GLOBAL_IGNORE_LIST = [
16
16
  '.tmp/',
17
17
  '.rollup.cache/',
18
18
  '.vscode/',
19
- '.wireit/',
20
19
  'amf-models/',
21
20
  'build/',
22
21
  'coverage/',
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@api-client/core",
3
3
  "description": "The API Client's core client library. Works in NodeJS and in a ES enabled browser.",
4
- "version": "0.19.8",
4
+ "version": "0.19.10",
5
5
  "license": "Apache-2.0",
6
6
  "exports": {
7
7
  "./browser.js": {
@@ -115,7 +115,6 @@
115
115
  "@japa/expect-type": "^2.0.3",
116
116
  "@japa/runner": "^5.3.0",
117
117
  "@pawel-up/semver": "^0.1.4",
118
- "@rollup/plugin-typescript": "^12.1.2",
119
118
  "@types/chai-as-promised": "^8.0.2",
120
119
  "@types/cors": "^2.8.12",
121
120
  "@types/express-ntlm": "^2.3.3",
@@ -125,6 +124,7 @@
125
124
  "@types/node": "^25.0.6",
126
125
  "@types/sinon": "^21.0.0",
127
126
  "@web/dev-server": "^0.4.6",
127
+ "@web/dev-server-esbuild": "^1.0.5",
128
128
  "@web/dev-server-rollup": "^0.6.4",
129
129
  "@web/test-runner": "^0.20.0",
130
130
  "@web/test-runner-playwright": "^0.11.0",
@@ -151,143 +151,38 @@
151
151
  "ts-lit-plugin": "^2.0.2",
152
152
  "ts-node-maintained": "^10.9.5",
153
153
  "typescript": "^5.5.2",
154
- "typescript-eslint": "^8.24.1",
155
- "wireit": "^0.14.4"
154
+ "typescript-eslint": "^8.24.1"
156
155
  },
157
156
  "scripts": {
158
- "build:browser": "wireit",
159
- "build:ts": "wireit",
160
- "build:node": "wireit",
157
+ "build:browser": "tsc --project tsconfig.browser.json",
158
+ "build:ts": "tsc --project tsconfig.json",
159
+ "build:node": "tsc --project tsconfig.node.json",
161
160
  "build": "npm run build:ts && npm run copy:assets",
162
161
  "prepare": "husky && npm run fixes && npm run build:ts && npm run build:api-models",
163
162
  "fixes": "node scripts/fix-rollup-plugin.js",
164
- "tsc": "wireit",
165
- "tsc:tests": "wireit",
166
- "tsc:watch": "wireit",
167
- "test:browser": "node --import ts-node-maintained/register/esm --enable-source-maps bin/test-web.ts --playwright --browsers chromium",
168
- "test:browser:watch": "node --import ts-node-maintained/register/esm --enable-source-maps bin/test-web.ts --watch --playwright --browsers chromium",
169
- "test": "wireit",
170
- "test:coverage": "wireit",
163
+ "tsc": "tsc",
164
+ "tsc:tests": "tsc --project tsconfig.browser.json",
165
+ "tsc:watch": "tsc --watch --project tsconfig.json",
166
+ "test:browser": "wtr --playwright --browsers chromium",
167
+ "test:browser:watch": "wtr --watch --playwright --browsers chromium",
168
+ "test": "npm run test:node && npm run test:browser",
169
+ "test:coverage": "npm run test:node:coverage && npm run test:browser",
171
170
  "test:node": "node --import ts-node-maintained/register/esm --enable-source-maps bin/test.ts",
172
171
  "test:node:coverage": "c8 --reporter lcov --reporter text node --import ts-node-maintained/register/esm --enable-source-maps bin/test.ts",
173
172
  "build:api-models": "node data/model.js",
174
173
  "copy:assets": "cp -f ./oauth-popup.html ./build/oauth-popup.html",
175
174
  "start": "echo \"Use the npm run dev instead\"",
176
175
  "lint": "npm run lint:prettier && npm run lint:eslint",
177
- "lint:eslint": "wireit",
178
- "lint:prettier": "wireit",
176
+ "lint:eslint": "eslint --color --cache --cache-location .eslintcache .",
177
+ "lint:prettier": "prettier \"**/*.ts\" --check",
179
178
  "format": "npm run format:prettier && npm run format:prettier",
180
- "format:prettier": "wireit",
181
- "format:eslint": "wireit",
179
+ "format:prettier": "prettier \"**/*.ts\" --write",
180
+ "format:eslint": "eslint --color --cache --fix --cache-location .eslintcache .",
182
181
  "release": "node scripts/release.js",
183
182
  "release:patch": "node scripts/release.js patch",
184
183
  "release:minor": "node scripts/release.js minor",
185
184
  "release:major": "node scripts/release.js major"
186
185
  },
187
- "wireit": {
188
- "test": {
189
- "command": "npm run test:node && npm run test:browser"
190
- },
191
- "test:coverage": {
192
- "command": "npm run test:node:coverage && npm run test:browser"
193
- },
194
- "tsc:watch": {
195
- "command": "tsc --watch --project tsconfig.json",
196
- "files": [
197
- "src/**/*.ts",
198
- "tsconfig.json"
199
- ],
200
- "output": [
201
- "build/**",
202
- ".tsbuildinfo"
203
- ]
204
- },
205
- "tsc": {
206
- "command": "tsc",
207
- "files": [
208
- "src/**/*.ts",
209
- "tsconfig.json"
210
- ],
211
- "output": [
212
- "build/**",
213
- ".tsbuildinfo"
214
- ]
215
- },
216
- "tsc:tests": {
217
- "command": "tsc --project tsconfig.browser.json",
218
- "clean": "if-file-deleted",
219
- "files": [
220
- "src/**/*.ts",
221
- "test/**/*.ts",
222
- "tests/**/*.ts",
223
- "tsconfig.browser.json"
224
- ],
225
- "output": [
226
- ".tmp/**",
227
- ".tmp/testing/tsconfig.browser.tsbuildinfo"
228
- ]
229
- },
230
- "build:ts": {
231
- "command": "tsc --project tsconfig.json",
232
- "clean": "if-file-deleted",
233
- "files": [
234
- "src/**/*.ts",
235
- "tsconfig.json"
236
- ],
237
- "output": [
238
- "build/**",
239
- "build/tsconfig.tsbuildinfo"
240
- ]
241
- },
242
- "build:browser": {
243
- "command": "tsc --project tsconfig.browser.json",
244
- "clean": "if-file-deleted",
245
- "files": [
246
- "src/**/*.ts",
247
- "test/**/*.ts",
248
- "tsconfig.browser.json"
249
- ],
250
- "output": [
251
- ".tmp/testing/**",
252
- ".tmp/testing/tsconfig.browser.tsbuildinfo"
253
- ]
254
- },
255
- "build:node": {
256
- "command": "tsc --project tsconfig.node.json",
257
- "clean": "if-file-deleted",
258
- "files": [
259
- "src/**/*.ts",
260
- "tests/**/*.ts",
261
- "tsconfig.node.json"
262
- ],
263
- "output": [
264
- ".tmp/node/**",
265
- ".tmp/node/tsconfig.node.tsbuildinfo"
266
- ]
267
- },
268
- "lint:eslint": {
269
- "command": "eslint --color --cache --cache-location .eslintcache .",
270
- "files": [
271
- "src/**/*.ts",
272
- "eslint.config.js"
273
- ],
274
- "output": []
275
- },
276
- "lint:prettier": {
277
- "command": "prettier \"**/*.ts\" --check"
278
- },
279
- "format:eslint": {
280
- "command": "eslint --color --cache --fix --cache-location .eslintcache .",
281
- "files": [
282
- "src/**/*.ts",
283
- "eslint.config.js"
284
- ],
285
- "output": []
286
- },
287
- "format:prettier": {
288
- "command": "prettier \"**/*.ts\" --write"
289
- }
290
- },
291
186
  "lint-staged": {
292
187
  "*.ts": [
293
188
  "npm run format"
@@ -166,6 +166,75 @@ export function observed(config: ObserveConfig = {}): PropertyDecorator {
166
166
  return target
167
167
  }
168
168
  const value = Reflect.get(target, prop)
169
+ if (typeof value === 'function') {
170
+ if (target instanceof Map || target instanceof Set) {
171
+ if (['set', 'add', 'delete', 'clear'].includes(prop as string)) {
172
+ return (...args: any[]) => {
173
+ if (prop === 'set' && (target as Map<any, any>).get(args[0]) === args[1]) {
174
+ return target
175
+ }
176
+ if (prop === 'delete' && !target.has(args[0])) {
177
+ return false
178
+ }
179
+ if (prop === 'clear' && target.size === 0) {
180
+ return undefined
181
+ }
182
+ const result = value.apply(target, args)
183
+ notifyChange()
184
+ return result
185
+ }
186
+ }
187
+ if (prop === 'get') {
188
+ return (...args: any[]) => {
189
+ const result = value.apply(target, args)
190
+ return createDeepProxy.bind(targetObject)(result, notifyChange)
191
+ }
192
+ }
193
+ if (prop === 'values' || prop === 'keys' || prop === 'entries') {
194
+ return (...args: any[]) => {
195
+ const iterator = value.apply(target, args)
196
+ return {
197
+ [Symbol.iterator]() {
198
+ return this
199
+ },
200
+ next() {
201
+ const result = iterator.next()
202
+ if (result.done) return result
203
+ if (prop === 'entries') {
204
+ return {
205
+ done: false,
206
+ value: [result.value[0], createDeepProxy.bind(targetObject)(result.value[1], notifyChange)],
207
+ }
208
+ }
209
+ if (prop === 'keys') {
210
+ return result
211
+ }
212
+ return {
213
+ done: false,
214
+ value: createDeepProxy.bind(targetObject)(result.value, notifyChange),
215
+ }
216
+ },
217
+ }
218
+ }
219
+ }
220
+ if (prop === 'forEach') {
221
+ return (callback: any, thisArg?: any) => {
222
+ value.call(target, (val: any, k: any, m: any) => {
223
+ callback.call(thisArg, createDeepProxy.bind(targetObject)(val, notifyChange), k, m)
224
+ })
225
+ }
226
+ }
227
+ return value.bind(target)
228
+ }
229
+ if (
230
+ target instanceof WeakMap ||
231
+ target instanceof WeakSet ||
232
+ target instanceof Date ||
233
+ target instanceof RegExp
234
+ ) {
235
+ return value.bind(target)
236
+ }
237
+ }
169
238
  return createDeepProxy.bind(targetObject)(value, notifyChange)
170
239
  },
171
240
  set(target, prop, value) {
@@ -188,6 +257,28 @@ export function observed(config: ObserveConfig = {}): PropertyDecorator {
188
257
 
189
258
  if (kind === 'accessor') {
190
259
  return {
260
+ init(this: DomainInstance, initialValue: V): V {
261
+ let map = Reflect.get(this, reactiveSymbol)
262
+ if (!map) {
263
+ map = {}
264
+ Reflect.set(this, reactiveSymbol, map)
265
+ }
266
+ const notify = () => {
267
+ if (this.domain) {
268
+ this.domain.notifyChange()
269
+ } else if (this.notifyChange) {
270
+ this.notifyChange()
271
+ }
272
+ }
273
+ let value = initialValue
274
+ if (deep) {
275
+ value = createDeepProxy.bind(this)(initialValue, notify)
276
+ }
277
+ if (value !== undefined) {
278
+ map[context.name] = value
279
+ }
280
+ return value
281
+ },
191
282
  set(this: DomainInstance, value: V): void {
192
283
  let map = Reflect.get(this, reactiveSymbol)
193
284
  if (!map) {
@@ -170,8 +170,10 @@ export class ApiModel extends DependentModel {
170
170
  /**
171
171
  * The specific subset of Data Entities to be exposed by this API.
172
172
  * These are the entities that are included in the data domain schema.
173
+ *
174
+ * The `key` is the key of the exposed entity. Using a Map to allow for quick lookups.
173
175
  */
174
- @observed({ deep: true }) accessor exposes: ExposedEntity[]
176
+ @observed({ deep: true }) accessor exposes: Map<string, ExposedEntity>
175
177
  /**
176
178
  * Optional array of access rules that define the access control policies
177
179
  * for the API. These rules are used to enforce security and permissions
@@ -306,9 +308,9 @@ export class ApiModel extends DependentModel {
306
308
  this.session = structuredClone(init.session)
307
309
  }
308
310
  if (Array.isArray(init.exposes)) {
309
- this.exposes = init.exposes.map((e) => new ExposedEntity(this, e))
311
+ this.exposes = new Map(init.exposes.map((e) => [e.key, new ExposedEntity(this, e)]))
310
312
  } else {
311
- this.exposes = []
313
+ this.exposes = new Map()
312
314
  }
313
315
  if (Array.isArray(init.accessRule)) {
314
316
  this.accessRule = init.accessRule.map((rule) => restoreAccessRule(rule))
@@ -338,7 +340,7 @@ export class ApiModel extends DependentModel {
338
340
  kind: this.kind,
339
341
  key: this.key,
340
342
  info: this.info.toJSON(),
341
- exposes: this.exposes.map((e) => e.toJSON()),
343
+ exposes: Array.from(this.exposes.values()).map((e) => e.toJSON()),
342
344
  }
343
345
  if (this.user) {
344
346
  result.user = { ...this.user }
@@ -408,9 +410,13 @@ export class ApiModel extends DependentModel {
408
410
  throw new Error(`No domain attached to API model`)
409
411
  }
410
412
  // checks whether the entity is already exposed as a root exposure.
411
- const existing = this.exposes.find(
412
- (e) => e.isRoot && e.entity.key === entity.key && e.entity.domain === entity.domain
413
- )
413
+ let existing: ExposedEntity | undefined
414
+ for (const exp of this.exposes.values()) {
415
+ if (exp.isRoot && exp.entity.key === entity.key) {
416
+ existing = exp
417
+ break
418
+ }
419
+ }
414
420
  if (existing) {
415
421
  // quietly return the existing exposure
416
422
  // TBD: should we throw an error here?
@@ -428,7 +434,7 @@ export class ApiModel extends DependentModel {
428
434
  // Check for root path collision and resolve by appending a number
429
435
  let counter = 1
430
436
  const originalCollectionPath = relativeCollectionPath
431
- while (this.exposes.some((e) => e.isRoot && e.collectionPath === relativeCollectionPath)) {
437
+ while (this.findCollectionPathCollision(relativeCollectionPath)) {
432
438
  relativeCollectionPath = `${originalCollectionPath}-${counter}`
433
439
  relativeResourcePath = `${relativeCollectionPath}/{id}`
434
440
  counter++
@@ -448,7 +454,7 @@ export class ApiModel extends DependentModel {
448
454
  newEntity.exposeOptions = { ...options }
449
455
  }
450
456
  const created = new ExposedEntity(this, newEntity)
451
- this.exposes.push(created)
457
+ this.exposes.set(created.key, created)
452
458
 
453
459
  // Follow associations if requested
454
460
  if (options?.followAssociations) {
@@ -493,7 +499,13 @@ export class ApiModel extends DependentModel {
493
499
  }
494
500
 
495
501
  // Check if this entity is ALREADY exposed anywhere in the model
496
- const existingExposure = this.getExposedEntity(target)
502
+ let existingExposure: ExposedEntity | undefined
503
+ for (const exp of this.exposes.values()) {
504
+ if (exp.entity.key === target.key) {
505
+ existingExposure = exp
506
+ break
507
+ }
508
+ }
497
509
 
498
510
  if (existingExposure) {
499
511
  // If it's already exposed and NOT root (i.e., it's currently a nested child of someone else),
@@ -509,7 +521,7 @@ export class ApiModel extends DependentModel {
509
521
 
510
522
  let counter = 1
511
523
  const originalCollectionPath = relativeCollectionPath
512
- while (this.exposes.some((e) => e.isRoot && e.collectionPath === relativeCollectionPath)) {
524
+ while (this.findResourcePathCollision(relativeCollectionPath)) {
513
525
  relativeCollectionPath = `${originalCollectionPath}-${counter}`
514
526
  relativeResourcePath = `${relativeCollectionPath}/{id}`
515
527
  counter++
@@ -556,7 +568,8 @@ export class ApiModel extends DependentModel {
556
568
  },
557
569
  }
558
570
 
559
- this.exposes.push(new ExposedEntity(this, nestedExposure))
571
+ const inst = new ExposedEntity(this, nestedExposure)
572
+ this.exposes.set(inst.key, inst)
560
573
  if (depth + 1 >= maxDepth) {
561
574
  nestedExposure.truncated = true
562
575
  } else {
@@ -580,29 +593,25 @@ export class ApiModel extends DependentModel {
580
593
  * @param key The key of the exposed entity to remove.
581
594
  */
582
595
  removeExposedEntity(key: string): void {
583
- const index = this.exposes.findIndex((e) => e.key === key)
584
- if (index < 0) {
596
+ if (!this.exposes.has(key)) {
585
597
  throw new Error(`Exposed entity with key "${key}" not found.`)
586
598
  }
587
599
  this.removeWithChildren(key)
588
600
  }
589
601
 
590
602
  private removeWithChildren(key: string): void {
591
- const index = this.exposes.findIndex((e) => e.key === key)
592
- if (index < 0) {
603
+ if (!this.exposes.has(key)) {
593
604
  return
594
605
  }
595
606
  // Remove the parent itself
596
- this.exposes.splice(index, 1)
607
+ this.exposes.delete(key)
597
608
  // Remove all children recursively
598
609
  const removeChildren = (parentKey: string) => {
599
610
  // Find all exposures whose parent.key matches parentKey
600
- const children = this.exposes.filter((e) => e.parent?.key === parentKey)
601
- for (const child of children) {
602
- removeChildren(child.key)
603
- const childIndex = this.exposes.findIndex((e) => e.key === child.key)
604
- if (childIndex >= 0) {
605
- this.exposes.splice(childIndex, 1)
611
+ for (const child of this.exposes.values()) {
612
+ if (child.parent?.key === parentKey) {
613
+ removeChildren(child.key)
614
+ this.exposes.delete(child.key)
606
615
  }
607
616
  }
608
617
  }
@@ -611,15 +620,6 @@ export class ApiModel extends DependentModel {
611
620
  this.notifyChange()
612
621
  }
613
622
 
614
- /**
615
- * Returns the exposed entity by its key.
616
- * @param entityKey The key of the entity to find.
617
- * @returns The exposed entity or undefined if not found.
618
- */
619
- getExposedEntity(entity: AssociationTarget): ExposedEntity | undefined {
620
- return this.exposes.find((e) => e.entity.key === entity.key && e.entity.domain === entity.domain)
621
- }
622
-
623
623
  /**
624
624
  * Clears the API model for a new data domain change.
625
625
  * This method resets the dependencies, exposes, user,
@@ -628,7 +628,7 @@ export class ApiModel extends DependentModel {
628
628
  cleanForDomainChange(): void {
629
629
  this.dependencies.clear()
630
630
  this.dependencyList = []
631
- this.exposes = []
631
+ this.exposes = new Map()
632
632
  this.user = undefined
633
633
  if (this.session) {
634
634
  this.session.properties = []
@@ -659,4 +659,44 @@ export class ApiModel extends DependentModel {
659
659
  this.dependencyList = [{ key: domain.key, version: domain.info.version }]
660
660
  this.notifyChange()
661
661
  }
662
+
663
+ /**
664
+ * Finds an existing root exposed entity that has the given resource path.
665
+ * Useful for detecting path collisions when adding or updating an exposed entity.
666
+ *
667
+ * @param path The resource path to check for collisions.
668
+ * @param ignore Optional key of an exposed entity to ignore during the check (usually the entity being updated).
669
+ * @returns The colliding `ExposedEntity` if found, otherwise `undefined`.
670
+ */
671
+ findResourcePathCollision(path: string, ignore?: string): ExposedEntity | undefined {
672
+ for (const e of this.exposes.values()) {
673
+ if (ignore && e.key === ignore) {
674
+ continue
675
+ }
676
+ if (e.isRoot && e.resourcePath === path) {
677
+ return e
678
+ }
679
+ }
680
+ return undefined
681
+ }
682
+
683
+ /**
684
+ * Finds an existing root exposed entity that has the given collection path.
685
+ * Useful for detecting path collisions when adding or updating an exposed entity.
686
+ *
687
+ * @param path The collection path to check for collisions.
688
+ * @param ignore Optional key of an exposed entity to ignore during the check (usually the entity being updated).
689
+ * @returns The colliding `ExposedEntity` if found, otherwise `undefined`.
690
+ */
691
+ findCollectionPathCollision(path: string, ignore?: string): ExposedEntity | undefined {
692
+ for (const e of this.exposes.values()) {
693
+ if (ignore && e.key === ignore) {
694
+ continue
695
+ }
696
+ if (e.isRoot && e.hasCollection && e.collectionPath === path) {
697
+ return e
698
+ }
699
+ }
700
+ return undefined
701
+ }
662
702
  }
@@ -1,6 +1,6 @@
1
1
  import { DataDomainKind } from '../models/kinds.js'
2
2
  import { DataDomain } from './DataDomain.js'
3
- import { DomainImpactReport } from './types.js'
3
+ import type { DomainImpactReport } from './types.js'
4
4
  import { AssociationValidation } from './validation/association_validation.js'
5
5
  import { EntityValidation } from './validation/entity_validation.js'
6
6
  import { PropertyValidation } from './validation/property_validation.js'