@twin.org/api-service 0.0.3-next.50 → 0.0.3-next.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/es/platformService.js +72 -29
- package/dist/es/platformService.js.map +1 -1
- package/docs/changelog.md +14 -0
- package/package.json +2 -2
|
@@ -16,12 +16,12 @@ export class PlatformService {
|
|
|
16
16
|
* The type of entity storage connector to use for tenant lookups, if multi-tenant.
|
|
17
17
|
* @internal
|
|
18
18
|
*/
|
|
19
|
-
|
|
19
|
+
_tenantEntityStorageConnectorType;
|
|
20
20
|
/**
|
|
21
21
|
* Entity storage connector used by the service.
|
|
22
22
|
* @internal
|
|
23
23
|
*/
|
|
24
|
-
|
|
24
|
+
_tenantEntityStorageConnector;
|
|
25
25
|
/**
|
|
26
26
|
* Indicates whether the service is running in a multi-tenant environment.
|
|
27
27
|
* @internal
|
|
@@ -32,7 +32,7 @@ export class PlatformService {
|
|
|
32
32
|
* @param options The options for the connector.
|
|
33
33
|
*/
|
|
34
34
|
constructor(options) {
|
|
35
|
-
this.
|
|
35
|
+
this._tenantEntityStorageConnectorType = options?.tenantEntityStorageType ?? "tenant";
|
|
36
36
|
this._isMultiTenant = options?.config?.isMultiTenant ?? false;
|
|
37
37
|
}
|
|
38
38
|
/**
|
|
@@ -56,20 +56,20 @@ export class PlatformService {
|
|
|
56
56
|
*/
|
|
57
57
|
async execute(method) {
|
|
58
58
|
if (this._isMultiTenant) {
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
const tenantEntityStorageConnector = this.ensureEntityStorageConnector();
|
|
60
|
+
if (!Is.empty(tenantEntityStorageConnector)) {
|
|
61
|
+
let cursor;
|
|
62
|
+
const baseContextIds = (await ContextIdStore.getContextIds()) ?? {};
|
|
63
|
+
do {
|
|
64
|
+
const result = await tenantEntityStorageConnector.query(undefined, undefined, ["id"], cursor);
|
|
65
|
+
for (const tenant of result.entities) {
|
|
66
|
+
await ContextIdStore.run({ ...baseContextIds, [ContextIdKeys.Tenant]: tenant.id }, async () => {
|
|
67
|
+
await method();
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
cursor = result.cursor;
|
|
71
|
+
} while (Is.stringValue(cursor));
|
|
61
72
|
}
|
|
62
|
-
let cursor;
|
|
63
|
-
const baseContextIds = (await ContextIdStore.getContextIds()) ?? {};
|
|
64
|
-
do {
|
|
65
|
-
const result = await this._entityStorageConnector.query(undefined, undefined, ["id"], cursor);
|
|
66
|
-
for (const tenant of result.entities) {
|
|
67
|
-
await ContextIdStore.run({ ...baseContextIds, [ContextIdKeys.Tenant]: tenant.id }, async () => {
|
|
68
|
-
await method();
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
cursor = result.cursor;
|
|
72
|
-
} while (Is.stringValue(cursor));
|
|
73
73
|
}
|
|
74
74
|
else {
|
|
75
75
|
await method();
|
|
@@ -87,26 +87,69 @@ export class PlatformService {
|
|
|
87
87
|
return undefined;
|
|
88
88
|
}
|
|
89
89
|
const contextIds = await ContextIdStore.getContextIds();
|
|
90
|
+
if (this._isMultiTenant) {
|
|
91
|
+
const tenantEntityStorageConnector = this.ensureEntityStorageConnector();
|
|
92
|
+
if (!Is.empty(tenantEntityStorageConnector)) {
|
|
93
|
+
// Post-#203 organization routing: when the URL carries an ?organization=<org-did>
|
|
94
|
+
// query param, that param — not the origin — identifies the target tenant. A single
|
|
95
|
+
// node hosts many tenants behind one shared origin, so an origin match alone cannot
|
|
96
|
+
// distinguish them and would incorrectly return the caller's own context. Resolve the
|
|
97
|
+
// tenant by its organization id (mirrors TenantProcessor inbound routing); when the
|
|
98
|
+
// organization is not a local tenant, treat the URL as remote (undefined).
|
|
99
|
+
const organizationId = HttpUrlHelper.getQueryStringParam(url, ContextIdKeys.Organization);
|
|
100
|
+
if (Is.stringValue(organizationId)) {
|
|
101
|
+
const orgTenant = await tenantEntityStorageConnector.get(organizationId, "organizationId");
|
|
102
|
+
if (!Is.empty(orgTenant)) {
|
|
103
|
+
return {
|
|
104
|
+
...contextIds,
|
|
105
|
+
[ContextIdKeys.Tenant]: orgTenant.id,
|
|
106
|
+
[ContextIdKeys.Organization]: orgTenant.organizationId,
|
|
107
|
+
...(Is.stringValue(orgTenant.publicOrigin)
|
|
108
|
+
? { [HttpContextIdKeys.PublicOrigin]: orgTenant.publicOrigin }
|
|
109
|
+
: {})
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
90
116
|
const publicOrigin = contextIds?.[HttpContextIdKeys.PublicOrigin];
|
|
91
117
|
if (publicOrigin === origin) {
|
|
92
118
|
return contextIds;
|
|
93
119
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
120
|
+
if (this._isMultiTenant) {
|
|
121
|
+
const tenantEntityStorageConnector = this.ensureEntityStorageConnector();
|
|
122
|
+
if (!Is.empty(tenantEntityStorageConnector)) {
|
|
123
|
+
const tenant = await tenantEntityStorageConnector.get(origin, "publicOrigin");
|
|
124
|
+
if (!Is.empty(tenant)) {
|
|
125
|
+
return {
|
|
126
|
+
...contextIds,
|
|
127
|
+
[ContextIdKeys.Tenant]: tenant.id,
|
|
128
|
+
[ContextIdKeys.Organization]: tenant.organizationId,
|
|
129
|
+
...(Is.stringValue(tenant.publicOrigin)
|
|
130
|
+
? { [HttpContextIdKeys.PublicOrigin]: tenant.publicOrigin }
|
|
131
|
+
: {})
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
97
135
|
}
|
|
98
|
-
|
|
99
|
-
|
|
136
|
+
else {
|
|
137
|
+
const localOrigin = contextIds?.[HttpContextIdKeys.LocalOrigin];
|
|
138
|
+
if (localOrigin === origin) {
|
|
139
|
+
return contextIds;
|
|
140
|
+
}
|
|
100
141
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Ensure that the entity storage connector is initialized and return it.
|
|
145
|
+
* @returns The entity storage connector.
|
|
146
|
+
* @internal
|
|
147
|
+
*/
|
|
148
|
+
ensureEntityStorageConnector() {
|
|
149
|
+
if (Is.empty(this._tenantEntityStorageConnector)) {
|
|
150
|
+
this._tenantEntityStorageConnector = EntityStorageConnectorFactory.getIfExists(this._tenantEntityStorageConnectorType);
|
|
109
151
|
}
|
|
152
|
+
return this._tenantEntityStorageConnector;
|
|
110
153
|
}
|
|
111
154
|
}
|
|
112
155
|
//# sourceMappingURL=platformService.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"platformService.js","sourceRoot":"","sources":["../../src/platformService.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2026 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport {\n\tHttpContextIdKeys,\n\tHttpUrlHelper,\n\ttype IPlatformComponent,\n\ttype ITenant\n} from \"@twin.org/api-models\";\nimport { ContextIdKeys, ContextIdStore, type IContextIds } from \"@twin.org/context\";\nimport { Guards, Is } from \"@twin.org/core\";\nimport {\n\tEntityStorageConnectorFactory,\n\ttype IEntityStorageConnector\n} from \"@twin.org/entity-storage-models\";\nimport { nameof } from \"@twin.org/nameof\";\nimport type { IPlatformServiceConstructorOptions } from \"./models/IPlatformServiceConstructorOptions.js\";\n\n/**\n * Service for performing platform operations.\n */\nexport class PlatformService implements IPlatformComponent {\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<PlatformService>();\n\n\t/**\n\t * The type of entity storage connector to use for tenant lookups, if multi-tenant.\n\t * @internal\n\t */\n\tprivate readonly _entityStorageConnectorType: string;\n\n\t/**\n\t * Entity storage connector used by the service.\n\t * @internal\n\t */\n\tprivate _entityStorageConnector?: IEntityStorageConnector<ITenant>;\n\n\t/**\n\t * Indicates whether the service is running in a multi-tenant environment.\n\t * @internal\n\t */\n\tprivate readonly _isMultiTenant: boolean;\n\n\t/**\n\t * Create a new instance of PlatformService.\n\t * @param options The options for the connector.\n\t */\n\tconstructor(options?: IPlatformServiceConstructorOptions) {\n\t\tthis._entityStorageConnectorType = options?.tenantEntityStorageType ?? \"tenant\";\n\t\tthis._isMultiTenant = options?.config?.isMultiTenant ?? false;\n\t}\n\n\t/**\n\t * Returns the class name of the component.\n\t * @returns The class name of the component.\n\t */\n\tpublic className(): string {\n\t\treturn PlatformService.CLASS_NAME;\n\t}\n\n\t/**\n\t * Indicates whether the component is running in a multi-tenant environment.\n\t * @returns True if the component is running in a multi-tenant environment, false otherwise.\n\t */\n\tpublic isMultiTenant(): boolean {\n\t\treturn this._isMultiTenant;\n\t}\n\n\t/**\n\t * Execute a method, if single tenant will run once, if multi-tenant will run for each tenant.\n\t * @param method The method to run for each tenant.\n\t * @returns A promise that resolves when the method has been executed for all applicable tenants.\n\t */\n\tpublic async execute(method: () => Promise<void>): Promise<void> {\n\t\tif (this._isMultiTenant) {\n\t\t\tif (Is.empty(this._entityStorageConnector)) {\n\t\t\t\tthis._entityStorageConnector = EntityStorageConnectorFactory.get(\n\t\t\t\t\tthis._entityStorageConnectorType\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tlet cursor: string | undefined;\n\n\t\t\tconst baseContextIds = (await ContextIdStore.getContextIds()) ?? {};\n\n\t\t\tdo {\n\t\t\t\tconst result = await this._entityStorageConnector.query(\n\t\t\t\t\tundefined,\n\t\t\t\t\tundefined,\n\t\t\t\t\t[\"id\"],\n\t\t\t\t\tcursor\n\t\t\t\t);\n\n\t\t\t\tfor (const tenant of result.entities) {\n\t\t\t\t\tawait ContextIdStore.run(\n\t\t\t\t\t\t{ ...baseContextIds, [ContextIdKeys.Tenant]: tenant.id },\n\t\t\t\t\t\tasync () => {\n\t\t\t\t\t\t\tawait method();\n\t\t\t\t\t\t}\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tcursor = result.cursor;\n\t\t\t} while (Is.stringValue(cursor));\n\t\t} else {\n\t\t\tawait method();\n\t\t}\n\t}\n\n\t/**\n\t * Get the local origin context IDs for the given URL.\n\t * @param url The URL to check.\n\t * @returns A promise that resolves to the context IDs if the URL is a local origin, undefined otherwise.\n\t */\n\tpublic async getLocalOriginContext(url: string): Promise<IContextIds | undefined> {\n\t\tGuards.stringValue(PlatformService.CLASS_NAME, nameof(url), url);\n\n\t\tconst origin = HttpUrlHelper.extractOrigin(url);\n\t\tif (!Is.stringValue(origin)) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst contextIds = await ContextIdStore.getContextIds();\n\t\tconst publicOrigin = contextIds?.[HttpContextIdKeys.PublicOrigin];\n\t\tif (publicOrigin === origin) {\n\t\t\treturn contextIds;\n\t\t}\n\n\t\tconst localOrigin = contextIds?.[HttpContextIdKeys.LocalOrigin];\n\t\tif (localOrigin === origin) {\n\t\t\treturn contextIds;\n\t\t}\n\n\t\tif (Is.empty(this._entityStorageConnector)) {\n\t\t\tthis._entityStorageConnector = EntityStorageConnectorFactory.get(\n\t\t\t\tthis._entityStorageConnectorType\n\t\t\t);\n\t\t}\n\n\t\tconst tenant = await this._entityStorageConnector.get(origin, \"publicOrigin\");\n\t\tif (!Is.empty(tenant)) {\n\t\t\treturn {\n\t\t\t\t...contextIds,\n\t\t\t\t[ContextIdKeys.Tenant]: tenant.id,\n\t\t\t\t[ContextIdKeys.Organization]: tenant.organizationId,\n\t\t\t\t[HttpContextIdKeys.PublicOrigin]: tenant.publicOrigin\n\t\t\t};\n\t\t}\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"platformService.js","sourceRoot":"","sources":["../../src/platformService.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2026 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport {\n\tHttpContextIdKeys,\n\tHttpUrlHelper,\n\ttype IPlatformComponent,\n\ttype ITenant\n} from \"@twin.org/api-models\";\nimport { ContextIdKeys, ContextIdStore, type IContextIds } from \"@twin.org/context\";\nimport { Guards, Is } from \"@twin.org/core\";\nimport {\n\tEntityStorageConnectorFactory,\n\ttype IEntityStorageConnector\n} from \"@twin.org/entity-storage-models\";\nimport { nameof } from \"@twin.org/nameof\";\nimport type { IPlatformServiceConstructorOptions } from \"./models/IPlatformServiceConstructorOptions.js\";\n\n/**\n * Service for performing platform operations.\n */\nexport class PlatformService implements IPlatformComponent {\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<PlatformService>();\n\n\t/**\n\t * The type of entity storage connector to use for tenant lookups, if multi-tenant.\n\t * @internal\n\t */\n\tprivate readonly _tenantEntityStorageConnectorType: string;\n\n\t/**\n\t * Entity storage connector used by the service.\n\t * @internal\n\t */\n\tprivate _tenantEntityStorageConnector?: IEntityStorageConnector<ITenant>;\n\n\t/**\n\t * Indicates whether the service is running in a multi-tenant environment.\n\t * @internal\n\t */\n\tprivate readonly _isMultiTenant: boolean;\n\n\t/**\n\t * Create a new instance of PlatformService.\n\t * @param options The options for the connector.\n\t */\n\tconstructor(options?: IPlatformServiceConstructorOptions) {\n\t\tthis._tenantEntityStorageConnectorType = options?.tenantEntityStorageType ?? \"tenant\";\n\t\tthis._isMultiTenant = options?.config?.isMultiTenant ?? false;\n\t}\n\n\t/**\n\t * Returns the class name of the component.\n\t * @returns The class name of the component.\n\t */\n\tpublic className(): string {\n\t\treturn PlatformService.CLASS_NAME;\n\t}\n\n\t/**\n\t * Indicates whether the component is running in a multi-tenant environment.\n\t * @returns True if the component is running in a multi-tenant environment, false otherwise.\n\t */\n\tpublic isMultiTenant(): boolean {\n\t\treturn this._isMultiTenant;\n\t}\n\n\t/**\n\t * Execute a method, if single tenant will run once, if multi-tenant will run for each tenant.\n\t * @param method The method to run for each tenant.\n\t * @returns A promise that resolves when the method has been executed for all applicable tenants.\n\t */\n\tpublic async execute(method: () => Promise<void>): Promise<void> {\n\t\tif (this._isMultiTenant) {\n\t\t\tconst tenantEntityStorageConnector = this.ensureEntityStorageConnector();\n\n\t\t\tif (!Is.empty(tenantEntityStorageConnector)) {\n\t\t\t\tlet cursor: string | undefined;\n\n\t\t\t\tconst baseContextIds = (await ContextIdStore.getContextIds()) ?? {};\n\n\t\t\t\tdo {\n\t\t\t\t\tconst result = await tenantEntityStorageConnector.query(\n\t\t\t\t\t\tundefined,\n\t\t\t\t\t\tundefined,\n\t\t\t\t\t\t[\"id\"],\n\t\t\t\t\t\tcursor\n\t\t\t\t\t);\n\n\t\t\t\t\tfor (const tenant of result.entities) {\n\t\t\t\t\t\tawait ContextIdStore.run(\n\t\t\t\t\t\t\t{ ...baseContextIds, [ContextIdKeys.Tenant]: tenant.id },\n\t\t\t\t\t\t\tasync () => {\n\t\t\t\t\t\t\t\tawait method();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\n\t\t\t\t\tcursor = result.cursor;\n\t\t\t\t} while (Is.stringValue(cursor));\n\t\t\t}\n\t\t} else {\n\t\t\tawait method();\n\t\t}\n\t}\n\n\t/**\n\t * Get the local origin context IDs for the given URL.\n\t * @param url The URL to check.\n\t * @returns A promise that resolves to the context IDs if the URL is a local origin, undefined otherwise.\n\t */\n\tpublic async getLocalOriginContext(url: string): Promise<IContextIds | undefined> {\n\t\tGuards.stringValue(PlatformService.CLASS_NAME, nameof(url), url);\n\n\t\tconst origin = HttpUrlHelper.extractOrigin(url);\n\t\tif (!Is.stringValue(origin)) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst contextIds = await ContextIdStore.getContextIds();\n\n\t\tif (this._isMultiTenant) {\n\t\t\tconst tenantEntityStorageConnector = this.ensureEntityStorageConnector();\n\t\t\tif (!Is.empty(tenantEntityStorageConnector)) {\n\t\t\t\t// Post-#203 organization routing: when the URL carries an ?organization=<org-did>\n\t\t\t\t// query param, that param — not the origin — identifies the target tenant. A single\n\t\t\t\t// node hosts many tenants behind one shared origin, so an origin match alone cannot\n\t\t\t\t// distinguish them and would incorrectly return the caller's own context. Resolve the\n\t\t\t\t// tenant by its organization id (mirrors TenantProcessor inbound routing); when the\n\t\t\t\t// organization is not a local tenant, treat the URL as remote (undefined).\n\t\t\t\tconst organizationId = HttpUrlHelper.getQueryStringParam(url, ContextIdKeys.Organization);\n\t\t\t\tif (Is.stringValue(organizationId)) {\n\t\t\t\t\tconst orgTenant = await tenantEntityStorageConnector.get(\n\t\t\t\t\t\torganizationId,\n\t\t\t\t\t\t\"organizationId\"\n\t\t\t\t\t);\n\t\t\t\t\tif (!Is.empty(orgTenant)) {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t...contextIds,\n\t\t\t\t\t\t\t[ContextIdKeys.Tenant]: orgTenant.id,\n\t\t\t\t\t\t\t[ContextIdKeys.Organization]: orgTenant.organizationId,\n\t\t\t\t\t\t\t...(Is.stringValue(orgTenant.publicOrigin)\n\t\t\t\t\t\t\t\t? { [HttpContextIdKeys.PublicOrigin]: orgTenant.publicOrigin }\n\t\t\t\t\t\t\t\t: {})\n\t\t\t\t\t\t};\n\t\t\t\t\t}\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst publicOrigin = contextIds?.[HttpContextIdKeys.PublicOrigin];\n\t\tif (publicOrigin === origin) {\n\t\t\treturn contextIds;\n\t\t}\n\n\t\tif (this._isMultiTenant) {\n\t\t\tconst tenantEntityStorageConnector = this.ensureEntityStorageConnector();\n\t\t\tif (!Is.empty(tenantEntityStorageConnector)) {\n\t\t\t\tconst tenant = await tenantEntityStorageConnector.get(origin, \"publicOrigin\");\n\t\t\t\tif (!Is.empty(tenant)) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\t...contextIds,\n\t\t\t\t\t\t[ContextIdKeys.Tenant]: tenant.id,\n\t\t\t\t\t\t[ContextIdKeys.Organization]: tenant.organizationId,\n\t\t\t\t\t\t...(Is.stringValue(tenant.publicOrigin)\n\t\t\t\t\t\t\t? { [HttpContextIdKeys.PublicOrigin]: tenant.publicOrigin }\n\t\t\t\t\t\t\t: {})\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tconst localOrigin = contextIds?.[HttpContextIdKeys.LocalOrigin];\n\t\t\tif (localOrigin === origin) {\n\t\t\t\treturn contextIds;\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Ensure that the entity storage connector is initialized and return it.\n\t * @returns The entity storage connector.\n\t * @internal\n\t */\n\tprivate ensureEntityStorageConnector(): IEntityStorageConnector<ITenant> | undefined {\n\t\tif (Is.empty(this._tenantEntityStorageConnector)) {\n\t\t\tthis._tenantEntityStorageConnector = EntityStorageConnectorFactory.getIfExists(\n\t\t\t\tthis._tenantEntityStorageConnectorType\n\t\t\t);\n\t\t}\n\n\t\treturn this._tenantEntityStorageConnector;\n\t}\n}\n"]}
|
package/docs/changelog.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.0.3-next.51](https://github.com/iotaledger/twin-api/compare/api-service-v0.0.3-next.50...api-service-v0.0.3-next.51) (2026-06-20)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* resolve local origin context by organization routing param ([#180](https://github.com/iotaledger/twin-api/issues/180)) ([bceb9f1](https://github.com/iotaledger/twin-api/commit/bceb9f1b5b68382b7e2f9743ee7b4ea0e3a33f55))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Dependencies
|
|
12
|
+
|
|
13
|
+
* The following workspace dependencies were updated
|
|
14
|
+
* dependencies
|
|
15
|
+
* @twin.org/api-models bumped from 0.0.3-next.50 to 0.0.3-next.51
|
|
16
|
+
|
|
3
17
|
## [0.0.3-next.50](https://github.com/iotaledger/twin-api/compare/api-service-v0.0.3-next.49...api-service-v0.0.3-next.50) (2026-06-19)
|
|
4
18
|
|
|
5
19
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twin.org/api-service",
|
|
3
|
-
"version": "0.0.3-next.
|
|
3
|
+
"version": "0.0.3-next.51",
|
|
4
4
|
"description": "Information and hosting service implementations with generated REST route handlers.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"node": ">=20.0.0"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@twin.org/api-models": "0.0.3-next.
|
|
17
|
+
"@twin.org/api-models": "0.0.3-next.51",
|
|
18
18
|
"@twin.org/context": "next",
|
|
19
19
|
"@twin.org/core": "next",
|
|
20
20
|
"@twin.org/engine-models": "next",
|