@ankhorage/data-sources 0.2.0 → 0.3.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.
- package/CHANGELOG.md +6 -0
- package/README.md +5 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/managed-api/index.d.ts +30 -0
- package/dist/managed-api/index.d.ts.map +1 -0
- package/dist/managed-api/index.js +257 -0
- package/dist/managed-api/index.js.map +1 -0
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/managed-api/index.test.ts +181 -0
- package/src/managed-api/index.ts +364 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @ankhorage/data-sources
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- f4266e8: Add managed API generation helpers that derive normalized database endpoint and CRUD operation configs from provider-neutral adapter references and collection definitions.
|
|
8
|
+
|
|
3
9
|
## 0.2.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
# @ankhorage/data-sources
|
|
5
5
|
|
|
6
|
-
         
|
|
7
7
|
|
|
8
8
|
Provider-neutral data source and endpoint system for APIs, databases, and runtime bindings.
|
|
9
9
|
|
|
@@ -15,11 +15,13 @@ Provider-neutral data source and endpoint system for APIs, databases, and runtim
|
|
|
15
15
|
- [Architecture overview](././paradox/diagrams/architecture-overview.mmd)
|
|
16
16
|
- [Module relationships](././paradox/diagrams/module-relationships.mmd)
|
|
17
17
|
- [Export graph](././paradox/diagrams/export-graph.mmd)
|
|
18
|
+
- [createManagedApiResourceSchema sequence](././paradox/diagrams/sequences/create-managed-api-resource-schema.mmd)
|
|
18
19
|
- [createManualRestDataSource sequence](././paradox/diagrams/sequences/create-manual-rest-data-source.mmd)
|
|
19
20
|
- [normalizeGraphQlIntrospectionOperations sequence](././paradox/diagrams/sequences/normalize-graph-ql-introspection-operations.mmd)
|
|
20
21
|
- [normalizeGraphQlIntrospectionSchemas sequence](././paradox/diagrams/sequences/normalize-graph-ql-introspection-schemas.mmd)
|
|
21
22
|
- [normalizeManualRestDataSource sequence](././paradox/diagrams/sequences/normalize-manual-rest-data-source.mmd)
|
|
22
23
|
- [normalizeOpenApiSchema sequence](././paradox/diagrams/sequences/normalize-open-api-schema.mmd)
|
|
24
|
+
- [validateManagedApiDefinition sequence](././paradox/diagrams/sequences/validate-managed-api-definition.mmd)
|
|
23
25
|
- [validateManualRestDataSource sequence](././paradox/diagrams/sequences/validate-manual-rest-data-source.mmd)
|
|
24
26
|
|
|
25
27
|
## Architecture preview
|
|
@@ -35,6 +37,8 @@ graph TD
|
|
|
35
37
|
module_src_graphql_index_ts["src/graphql/index.ts"]
|
|
36
38
|
package__ankhorage_data_sources -.-> module_src_graphql_index_ts
|
|
37
39
|
module_src_index_ts["src/index.ts"]
|
|
40
|
+
module_src_managed_api_index_ts["src/managed-api/index.ts"]
|
|
41
|
+
package__ankhorage_data_sources -.-> module_src_managed_api_index_ts
|
|
38
42
|
module_src_openapi_index_ts["src/openapi/index.ts"]
|
|
39
43
|
package__ankhorage_data_sources -.-> module_src_openapi_index_ts
|
|
40
44
|
module_src_rest_index_ts["src/rest/index.ts"]
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { DataSourceConfig, DataSourceKind } from '@ankhorage/contracts/data';
|
|
2
2
|
export * from './graphql';
|
|
3
|
+
export * from './managed-api';
|
|
3
4
|
export * from './openapi';
|
|
4
5
|
export * from './rest';
|
|
5
6
|
export declare const DATA_SOURCES_PACKAGE_NAME: "@ankhorage/data-sources";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAElF,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,QAAQ,CAAC;AAEvB,eAAO,MAAM,yBAAyB,EAAG,yBAAkC,CAAC;AAE5E,eAAO,MAAM,2BAA2B,oEAMM,CAAC;AAE/C,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,WAAW,EAAE,OAAO,yBAAyB,CAAC;IACvD,QAAQ,CAAC,cAAc,EAAE,OAAO,2BAA2B,CAAC;CAC7D;AAED,wBAAgB,yBAAyB,IAAI,sBAAsB,CAKlE;AAED,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,IAAI,cAAc,CAE9E;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,gBAAgB,GAAG,cAAc,CAE1E"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAElF,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC;AAC1B,cAAc,QAAQ,CAAC;AAEvB,eAAO,MAAM,yBAAyB,EAAG,yBAAkC,CAAC;AAE5E,eAAO,MAAM,2BAA2B,oEAMM,CAAC;AAE/C,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,WAAW,EAAE,OAAO,yBAAyB,CAAC;IACvD,QAAQ,CAAC,cAAc,EAAE,OAAO,2BAA2B,CAAC;CAC7D;AAED,wBAAgB,yBAAyB,IAAI,sBAAsB,CAKlE;AAED,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,IAAI,cAAc,CAE9E;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,gBAAgB,GAAG,cAAc,CAE1E"}
|
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,QAAQ,CAAC;AAEvB,MAAM,CAAC,MAAM,yBAAyB,GAAG,yBAAkC,CAAC;AAE5E,MAAM,CAAC,MAAM,2BAA2B,GAAG;IACzC,UAAU;IACV,SAAS;IACT,aAAa;IACb,SAAS;IACT,MAAM;CACsC,CAAC;AAO/C,MAAM,UAAU,yBAAyB;IACvC,OAAO;QACL,WAAW,EAAE,yBAAyB;QACtC,cAAc,EAAE,2BAA2B;KAC5C,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,yBAAyB,CAAC,IAAY;IACpD,OAAO,2BAA2B,CAAC,IAAI,CAAC,CAAC,aAAa,EAAE,EAAE,CAAC,aAAa,KAAK,IAAI,CAAC,CAAC;AACrF,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,MAAwB;IACxD,OAAO,MAAM,CAAC,IAAI,CAAC;AACrB,CAAC","sourcesContent":["import type { DataSourceConfig, DataSourceKind } from '@ankhorage/contracts/data';\n\nexport * from './graphql';\nexport * from './openapi';\nexport * from './rest';\n\nexport const DATA_SOURCES_PACKAGE_NAME = '@ankhorage/data-sources' as const;\n\nexport const SUPPORTED_DATA_SOURCE_KINDS = [\n 'database',\n 'graphql',\n 'managed-api',\n 'openapi',\n 'rest',\n] as const satisfies readonly DataSourceKind[];\n\nexport interface DataSourcesPackageInfo {\n readonly packageName: typeof DATA_SOURCES_PACKAGE_NAME;\n readonly supportedKinds: typeof SUPPORTED_DATA_SOURCE_KINDS;\n}\n\nexport function getDataSourcesPackageInfo(): DataSourcesPackageInfo {\n return {\n packageName: DATA_SOURCES_PACKAGE_NAME,\n supportedKinds: SUPPORTED_DATA_SOURCE_KINDS,\n };\n}\n\nexport function isSupportedDataSourceKind(kind: string): kind is DataSourceKind {\n return SUPPORTED_DATA_SOURCE_KINDS.some((supportedKind) => supportedKind === kind);\n}\n\nexport function getDataSourceKind(source: DataSourceConfig): DataSourceKind {\n return source.kind;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC;AAC1B,cAAc,QAAQ,CAAC;AAEvB,MAAM,CAAC,MAAM,yBAAyB,GAAG,yBAAkC,CAAC;AAE5E,MAAM,CAAC,MAAM,2BAA2B,GAAG;IACzC,UAAU;IACV,SAAS;IACT,aAAa;IACb,SAAS;IACT,MAAM;CACsC,CAAC;AAO/C,MAAM,UAAU,yBAAyB;IACvC,OAAO;QACL,WAAW,EAAE,yBAAyB;QACtC,cAAc,EAAE,2BAA2B;KAC5C,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,yBAAyB,CAAC,IAAY;IACpD,OAAO,2BAA2B,CAAC,IAAI,CAAC,CAAC,aAAa,EAAE,EAAE,CAAC,aAAa,KAAK,IAAI,CAAC,CAAC;AACrF,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,MAAwB;IACxD,OAAO,MAAM,CAAC,IAAI,CAAC;AACrB,CAAC","sourcesContent":["import type { DataSourceConfig, DataSourceKind } from '@ankhorage/contracts/data';\n\nexport * from './graphql';\nexport * from './managed-api';\nexport * from './openapi';\nexport * from './rest';\n\nexport const DATA_SOURCES_PACKAGE_NAME = '@ankhorage/data-sources' as const;\n\nexport const SUPPORTED_DATA_SOURCE_KINDS = [\n 'database',\n 'graphql',\n 'managed-api',\n 'openapi',\n 'rest',\n] as const satisfies readonly DataSourceKind[];\n\nexport interface DataSourcesPackageInfo {\n readonly packageName: typeof DATA_SOURCES_PACKAGE_NAME;\n readonly supportedKinds: typeof SUPPORTED_DATA_SOURCE_KINDS;\n}\n\nexport function getDataSourcesPackageInfo(): DataSourcesPackageInfo {\n return {\n packageName: DATA_SOURCES_PACKAGE_NAME,\n supportedKinds: SUPPORTED_DATA_SOURCE_KINDS,\n };\n}\n\nexport function isSupportedDataSourceKind(kind: string): kind is DataSourceKind {\n return SUPPORTED_DATA_SOURCE_KINDS.some((supportedKind) => supportedKind === kind);\n}\n\nexport function getDataSourceKind(source: DataSourceConfig): DataSourceKind {\n return source.kind;\n}\n"]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { AdapterRef, DataEndpointConfig, DataOperationConfig, DataSchema, DataSourceDiagnostic, DataSourceDiagnosticResult, ManagedApiDataSourceConfig, OperationId } from '@ankhorage/contracts/data';
|
|
2
|
+
import type { DbCollectionDefinition } from '@ankhorage/contracts/db';
|
|
3
|
+
export declare const MANAGED_API_CRUD_OPERATIONS: readonly ["list", "read", "create", "update", "delete"];
|
|
4
|
+
export type ManagedApiCrudOperation = (typeof MANAGED_API_CRUD_OPERATIONS)[number];
|
|
5
|
+
export interface ManagedApiOperationPolicyRef {
|
|
6
|
+
readonly id: string;
|
|
7
|
+
readonly operation?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ManagedApiGenerationResourceDefinition {
|
|
10
|
+
readonly name: string;
|
|
11
|
+
readonly collection: DbCollectionDefinition;
|
|
12
|
+
readonly operations?: readonly ManagedApiCrudOperation[];
|
|
13
|
+
readonly policies?: readonly ManagedApiOperationPolicyRef[];
|
|
14
|
+
}
|
|
15
|
+
export interface ManagedApiGenerationDefinition {
|
|
16
|
+
readonly id: string;
|
|
17
|
+
readonly adapter: AdapterRef;
|
|
18
|
+
readonly name?: string;
|
|
19
|
+
readonly description?: string;
|
|
20
|
+
readonly resources: readonly ManagedApiGenerationResourceDefinition[];
|
|
21
|
+
}
|
|
22
|
+
export declare function createManagedApiDataSource(definition: ManagedApiGenerationDefinition): DataSourceDiagnosticResult<ManagedApiDataSourceConfig>;
|
|
23
|
+
export declare function validateManagedApiDefinition(definition: ManagedApiGenerationDefinition): readonly DataSourceDiagnostic[];
|
|
24
|
+
export declare function normalizeManagedApiDataSource(definition: ManagedApiGenerationDefinition): ManagedApiDataSourceConfig;
|
|
25
|
+
export declare function createManagedApiEndpoint(definition: ManagedApiGenerationDefinition, resource: ManagedApiGenerationResourceDefinition, operations?: readonly ManagedApiCrudOperation[]): DataEndpointConfig;
|
|
26
|
+
export declare function createManagedApiOperation(definition: ManagedApiGenerationDefinition, resource: ManagedApiGenerationResourceDefinition, operation: ManagedApiCrudOperation): DataOperationConfig;
|
|
27
|
+
export declare function getManagedApiOperations(resource: ManagedApiGenerationResourceDefinition): readonly ManagedApiCrudOperation[];
|
|
28
|
+
export declare function createManagedApiOperationId(resourceName: string, operation: ManagedApiCrudOperation): OperationId;
|
|
29
|
+
export declare function createManagedApiResourceSchema(collection: DbCollectionDefinition): DataSchema;
|
|
30
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/managed-api/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EAEV,kBAAkB,EAClB,mBAAmB,EAGnB,UAAU,EACV,oBAAoB,EACpB,0BAA0B,EAC1B,0BAA0B,EAE1B,WAAW,EACZ,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EACV,sBAAsB,EAGvB,MAAM,yBAAyB,CAAC;AAEjC,eAAO,MAAM,2BAA2B,yDAA0D,CAAC;AAEnG,MAAM,MAAM,uBAAuB,GAAG,CAAC,OAAO,2BAA2B,CAAC,CAAC,MAAM,CAAC,CAAC;AAEnF,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,sCAAsC;IACrD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,UAAU,EAAE,sBAAsB,CAAC;IAC5C,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,uBAAuB,EAAE,CAAC;IACzD,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,4BAA4B,EAAE,CAAC;CAC7D;AAED,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAC;IAC7B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,SAAS,EAAE,SAAS,sCAAsC,EAAE,CAAC;CACvE;AAED,wBAAgB,0BAA0B,CACxC,UAAU,EAAE,8BAA8B,GACzC,0BAA0B,CAAC,0BAA0B,CAAC,CAWxD;AAED,wBAAgB,4BAA4B,CAC1C,UAAU,EAAE,8BAA8B,GACzC,SAAS,oBAAoB,EAAE,CAsDjC;AAED,wBAAgB,6BAA6B,CAC3C,UAAU,EAAE,8BAA8B,GACzC,0BAA0B,CA6B5B;AAED,wBAAgB,wBAAwB,CACtC,UAAU,EAAE,8BAA8B,EAC1C,QAAQ,EAAE,sCAAsC,EAChD,UAAU,GAAE,SAAS,uBAAuB,EAAsC,GACjF,kBAAkB,CAmBpB;AAED,wBAAgB,yBAAyB,CACvC,UAAU,EAAE,8BAA8B,EAC1C,QAAQ,EAAE,sCAAsC,EAChD,SAAS,EAAE,uBAAuB,GACjC,mBAAmB,CAsBrB;AAED,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,sCAAsC,GAC/C,SAAS,uBAAuB,EAAE,CAEpC;AAED,wBAAgB,2BAA2B,CACzC,YAAY,EAAE,MAAM,EACpB,SAAS,EAAE,uBAAuB,GACjC,WAAW,CAEb;AAED,wBAAgB,8BAA8B,CAAC,UAAU,EAAE,sBAAsB,GAAG,UAAU,CAe7F"}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
export const MANAGED_API_CRUD_OPERATIONS = ['list', 'read', 'create', 'update', 'delete'];
|
|
2
|
+
export function createManagedApiDataSource(definition) {
|
|
3
|
+
const diagnostics = validateManagedApiDefinition(definition);
|
|
4
|
+
const hasErrors = diagnostics.some((diagnostic) => diagnostic.severity === 'error');
|
|
5
|
+
if (hasErrors)
|
|
6
|
+
return { ok: false, diagnostics };
|
|
7
|
+
return {
|
|
8
|
+
ok: true,
|
|
9
|
+
data: normalizeManagedApiDataSource(definition),
|
|
10
|
+
diagnostics,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function validateManagedApiDefinition(definition) {
|
|
14
|
+
const diagnostics = [];
|
|
15
|
+
if (definition.resources.length === 0) {
|
|
16
|
+
diagnostics.push({
|
|
17
|
+
code: 'invalid-config',
|
|
18
|
+
dataSourceId: definition.id,
|
|
19
|
+
message: 'Managed API generation requires at least one resource.',
|
|
20
|
+
path: 'resources',
|
|
21
|
+
severity: 'error',
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
for (const resource of definition.resources) {
|
|
25
|
+
if (resource.name.trim().length === 0) {
|
|
26
|
+
diagnostics.push({
|
|
27
|
+
code: 'invalid-config',
|
|
28
|
+
dataSourceId: definition.id,
|
|
29
|
+
endpointId: resource.name,
|
|
30
|
+
message: 'Managed API resource names must be non-empty.',
|
|
31
|
+
path: 'resources.name',
|
|
32
|
+
severity: 'error',
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
if (resource.collection.name.trim().length === 0) {
|
|
36
|
+
diagnostics.push({
|
|
37
|
+
code: 'invalid-config',
|
|
38
|
+
dataSourceId: definition.id,
|
|
39
|
+
endpointId: resource.name,
|
|
40
|
+
message: 'Managed API resource collections must have a non-empty name.',
|
|
41
|
+
path: `resources.${resource.name}.collection.name`,
|
|
42
|
+
severity: 'error',
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
const primaryKey = resolveManagedApiPrimaryKey(resource.collection);
|
|
46
|
+
if (primaryKey === undefined &&
|
|
47
|
+
getManagedApiOperations(resource).some((operation) => operation !== 'list')) {
|
|
48
|
+
diagnostics.push({
|
|
49
|
+
code: 'invalid-config',
|
|
50
|
+
dataSourceId: definition.id,
|
|
51
|
+
endpointId: resource.name,
|
|
52
|
+
message: 'Managed API resources need a primaryKey field when generating read/create/update/delete operations.',
|
|
53
|
+
path: `resources.${resource.name}.collection.primaryKey`,
|
|
54
|
+
severity: 'error',
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return diagnostics;
|
|
59
|
+
}
|
|
60
|
+
export function normalizeManagedApiDataSource(definition) {
|
|
61
|
+
const resources = [];
|
|
62
|
+
const endpoints = {};
|
|
63
|
+
const schemas = {};
|
|
64
|
+
for (const resource of definition.resources) {
|
|
65
|
+
const operations = getManagedApiOperations(resource);
|
|
66
|
+
const resourceConfig = {
|
|
67
|
+
name: resource.name,
|
|
68
|
+
collection: resource.collection,
|
|
69
|
+
operations,
|
|
70
|
+
metadata: createManagedApiResourceMetadata(resource),
|
|
71
|
+
};
|
|
72
|
+
resources.push(resourceConfig);
|
|
73
|
+
endpoints[resource.name] = createManagedApiEndpoint(definition, resource, operations);
|
|
74
|
+
schemas[resource.name] = createManagedApiResourceSchema(resource.collection);
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
id: definition.id,
|
|
78
|
+
kind: 'managed-api',
|
|
79
|
+
name: definition.name,
|
|
80
|
+
description: definition.description,
|
|
81
|
+
adapter: definition.adapter,
|
|
82
|
+
resources,
|
|
83
|
+
endpoints,
|
|
84
|
+
schemas,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export function createManagedApiEndpoint(definition, resource, operations = getManagedApiOperations(resource)) {
|
|
88
|
+
const operationConfigs = {};
|
|
89
|
+
for (const operation of operations) {
|
|
90
|
+
const operationConfig = createManagedApiOperation(definition, resource, operation);
|
|
91
|
+
operationConfigs[operationConfig.id] = operationConfig;
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
id: resource.name,
|
|
95
|
+
kind: 'database',
|
|
96
|
+
operations: operationConfigs,
|
|
97
|
+
metadata: {
|
|
98
|
+
adapterId: definition.adapter.id,
|
|
99
|
+
collection: resource.collection.name,
|
|
100
|
+
schema: resource.collection.schema ?? null,
|
|
101
|
+
source: 'managed-api',
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
export function createManagedApiOperation(definition, resource, operation) {
|
|
106
|
+
const operationId = createManagedApiOperationId(resource.name, operation);
|
|
107
|
+
const primaryKey = resolveManagedApiPrimaryKey(resource.collection);
|
|
108
|
+
const primaryKeyParameter = primaryKey === undefined ? undefined : createPrimaryKeyParameter(primaryKey);
|
|
109
|
+
return {
|
|
110
|
+
id: operationId,
|
|
111
|
+
endpointId: resource.name,
|
|
112
|
+
name: createManagedApiOperationName(resource.name, operation),
|
|
113
|
+
protocol: 'database',
|
|
114
|
+
intent: mapManagedApiOperationIntent(operation),
|
|
115
|
+
request: createManagedApiOperationRequest(resource.collection, operation, primaryKeyParameter),
|
|
116
|
+
response: createManagedApiOperationResponse(resource.collection, operation),
|
|
117
|
+
metadata: {
|
|
118
|
+
adapterId: definition.adapter.id,
|
|
119
|
+
collection: resource.collection.name,
|
|
120
|
+
operation,
|
|
121
|
+
schema: resource.collection.schema ?? null,
|
|
122
|
+
source: 'managed-api',
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
export function getManagedApiOperations(resource) {
|
|
127
|
+
return resource.operations ?? MANAGED_API_CRUD_OPERATIONS;
|
|
128
|
+
}
|
|
129
|
+
export function createManagedApiOperationId(resourceName, operation) {
|
|
130
|
+
return `${resourceName}.${operation}`;
|
|
131
|
+
}
|
|
132
|
+
export function createManagedApiResourceSchema(collection) {
|
|
133
|
+
const properties = {};
|
|
134
|
+
const required = [];
|
|
135
|
+
for (const field of collection.fields) {
|
|
136
|
+
properties[field.name] = createManagedApiFieldSchema(field);
|
|
137
|
+
if (field.required === true)
|
|
138
|
+
required.push(field.name);
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
type: 'object',
|
|
142
|
+
title: collection.name,
|
|
143
|
+
required: required.length > 0 ? required : undefined,
|
|
144
|
+
properties,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function createManagedApiResourceMetadata(resource) {
|
|
148
|
+
if (resource.policies === undefined)
|
|
149
|
+
return undefined;
|
|
150
|
+
return {
|
|
151
|
+
policies: resource.policies.map((policy) => {
|
|
152
|
+
const metadata = {
|
|
153
|
+
id: policy.id,
|
|
154
|
+
};
|
|
155
|
+
if (policy.operation !== undefined) {
|
|
156
|
+
metadata.operation = policy.operation;
|
|
157
|
+
}
|
|
158
|
+
return metadata;
|
|
159
|
+
}),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function createManagedApiOperationName(resourceName, operation) {
|
|
163
|
+
return `${operation} ${resourceName}`;
|
|
164
|
+
}
|
|
165
|
+
function mapManagedApiOperationIntent(operation) {
|
|
166
|
+
if (operation === 'list' || operation === 'read')
|
|
167
|
+
return 'read';
|
|
168
|
+
if (operation === 'create')
|
|
169
|
+
return 'create';
|
|
170
|
+
if (operation === 'update')
|
|
171
|
+
return 'update';
|
|
172
|
+
return 'delete';
|
|
173
|
+
}
|
|
174
|
+
function createManagedApiOperationRequest(collection, operation, primaryKeyParameter) {
|
|
175
|
+
if (operation === 'list') {
|
|
176
|
+
return {
|
|
177
|
+
parameters: [
|
|
178
|
+
{
|
|
179
|
+
name: 'limit',
|
|
180
|
+
location: 'query',
|
|
181
|
+
schema: { type: 'integer' },
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
name: 'offset',
|
|
185
|
+
location: 'query',
|
|
186
|
+
schema: { type: 'integer' },
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
if (operation === 'read' || operation === 'delete') {
|
|
192
|
+
return primaryKeyParameter === undefined ? undefined : { parameters: [primaryKeyParameter] };
|
|
193
|
+
}
|
|
194
|
+
const parameters = operation === 'update' && primaryKeyParameter !== undefined ? [primaryKeyParameter] : undefined;
|
|
195
|
+
return {
|
|
196
|
+
parameters,
|
|
197
|
+
schema: createManagedApiResourceSchema(collection),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function createManagedApiOperationResponse(collection, operation) {
|
|
201
|
+
const resourceSchema = createManagedApiResourceSchema(collection);
|
|
202
|
+
if (operation === 'list') {
|
|
203
|
+
return {
|
|
204
|
+
schema: {
|
|
205
|
+
type: 'array',
|
|
206
|
+
items: resourceSchema,
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
if (operation === 'delete') {
|
|
211
|
+
return {
|
|
212
|
+
schema: {
|
|
213
|
+
type: 'object',
|
|
214
|
+
properties: {
|
|
215
|
+
deleted: { type: 'boolean' },
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
return { schema: resourceSchema };
|
|
221
|
+
}
|
|
222
|
+
function createPrimaryKeyParameter(primaryKey) {
|
|
223
|
+
return {
|
|
224
|
+
name: primaryKey,
|
|
225
|
+
location: 'path',
|
|
226
|
+
required: true,
|
|
227
|
+
schema: {
|
|
228
|
+
type: 'string',
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
function resolveManagedApiPrimaryKey(collection) {
|
|
233
|
+
if (collection.primaryKey !== undefined)
|
|
234
|
+
return collection.primaryKey;
|
|
235
|
+
return collection.fields.find((field) => field.unique === true)?.name;
|
|
236
|
+
}
|
|
237
|
+
function createManagedApiFieldSchema(field) {
|
|
238
|
+
const schema = mapDbFieldTypeToDataSchema(field.type);
|
|
239
|
+
return {
|
|
240
|
+
...schema,
|
|
241
|
+
default: field.defaultValue,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
function mapDbFieldTypeToDataSchema(type) {
|
|
245
|
+
if (type === 'boolean')
|
|
246
|
+
return { type: 'boolean' };
|
|
247
|
+
if (type === 'datetime')
|
|
248
|
+
return { type: 'string', format: 'date-time' };
|
|
249
|
+
if (type === 'json')
|
|
250
|
+
return { type: 'object' };
|
|
251
|
+
if (type === 'number')
|
|
252
|
+
return { type: 'number' };
|
|
253
|
+
if (type === 'uuid')
|
|
254
|
+
return { type: 'string', format: 'uuid' };
|
|
255
|
+
return { type: 'string' };
|
|
256
|
+
}
|
|
257
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/managed-api/index.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n AdapterRef,\n DataContractValue,\n DataEndpointConfig,\n DataOperationConfig,\n DataOperationIntent,\n DataOperationParameter,\n DataSchema,\n DataSourceDiagnostic,\n DataSourceDiagnosticResult,\n ManagedApiDataSourceConfig,\n ManagedApiResourceConfig,\n OperationId,\n} from '@ankhorage/contracts/data';\nimport type {\n DbCollectionDefinition,\n DbFieldDefinition,\n DbFieldType,\n} from '@ankhorage/contracts/db';\n\nexport const MANAGED_API_CRUD_OPERATIONS = ['list', 'read', 'create', 'update', 'delete'] as const;\n\nexport type ManagedApiCrudOperation = (typeof MANAGED_API_CRUD_OPERATIONS)[number];\n\nexport interface ManagedApiOperationPolicyRef {\n readonly id: string;\n readonly operation?: string;\n}\n\nexport interface ManagedApiGenerationResourceDefinition {\n readonly name: string;\n readonly collection: DbCollectionDefinition;\n readonly operations?: readonly ManagedApiCrudOperation[];\n readonly policies?: readonly ManagedApiOperationPolicyRef[];\n}\n\nexport interface ManagedApiGenerationDefinition {\n readonly id: string;\n readonly adapter: AdapterRef;\n readonly name?: string;\n readonly description?: string;\n readonly resources: readonly ManagedApiGenerationResourceDefinition[];\n}\n\nexport function createManagedApiDataSource(\n definition: ManagedApiGenerationDefinition,\n): DataSourceDiagnosticResult<ManagedApiDataSourceConfig> {\n const diagnostics = validateManagedApiDefinition(definition);\n const hasErrors = diagnostics.some((diagnostic) => diagnostic.severity === 'error');\n\n if (hasErrors) return { ok: false, diagnostics };\n\n return {\n ok: true,\n data: normalizeManagedApiDataSource(definition),\n diagnostics,\n };\n}\n\nexport function validateManagedApiDefinition(\n definition: ManagedApiGenerationDefinition,\n): readonly DataSourceDiagnostic[] {\n const diagnostics: DataSourceDiagnostic[] = [];\n\n if (definition.resources.length === 0) {\n diagnostics.push({\n code: 'invalid-config',\n dataSourceId: definition.id,\n message: 'Managed API generation requires at least one resource.',\n path: 'resources',\n severity: 'error',\n });\n }\n\n for (const resource of definition.resources) {\n if (resource.name.trim().length === 0) {\n diagnostics.push({\n code: 'invalid-config',\n dataSourceId: definition.id,\n endpointId: resource.name,\n message: 'Managed API resource names must be non-empty.',\n path: 'resources.name',\n severity: 'error',\n });\n }\n\n if (resource.collection.name.trim().length === 0) {\n diagnostics.push({\n code: 'invalid-config',\n dataSourceId: definition.id,\n endpointId: resource.name,\n message: 'Managed API resource collections must have a non-empty name.',\n path: `resources.${resource.name}.collection.name`,\n severity: 'error',\n });\n }\n\n const primaryKey = resolveManagedApiPrimaryKey(resource.collection);\n if (\n primaryKey === undefined &&\n getManagedApiOperations(resource).some((operation) => operation !== 'list')\n ) {\n diagnostics.push({\n code: 'invalid-config',\n dataSourceId: definition.id,\n endpointId: resource.name,\n message:\n 'Managed API resources need a primaryKey field when generating read/create/update/delete operations.',\n path: `resources.${resource.name}.collection.primaryKey`,\n severity: 'error',\n });\n }\n }\n\n return diagnostics;\n}\n\nexport function normalizeManagedApiDataSource(\n definition: ManagedApiGenerationDefinition,\n): ManagedApiDataSourceConfig {\n const resources: ManagedApiResourceConfig[] = [];\n const endpoints: Record<string, DataEndpointConfig> = {};\n const schemas: Record<string, DataSchema> = {};\n\n for (const resource of definition.resources) {\n const operations = getManagedApiOperations(resource);\n const resourceConfig: ManagedApiResourceConfig = {\n name: resource.name,\n collection: resource.collection,\n operations,\n metadata: createManagedApiResourceMetadata(resource),\n };\n\n resources.push(resourceConfig);\n endpoints[resource.name] = createManagedApiEndpoint(definition, resource, operations);\n schemas[resource.name] = createManagedApiResourceSchema(resource.collection);\n }\n\n return {\n id: definition.id,\n kind: 'managed-api',\n name: definition.name,\n description: definition.description,\n adapter: definition.adapter,\n resources,\n endpoints,\n schemas,\n };\n}\n\nexport function createManagedApiEndpoint(\n definition: ManagedApiGenerationDefinition,\n resource: ManagedApiGenerationResourceDefinition,\n operations: readonly ManagedApiCrudOperation[] = getManagedApiOperations(resource),\n): DataEndpointConfig {\n const operationConfigs: Record<OperationId, DataOperationConfig> = {};\n\n for (const operation of operations) {\n const operationConfig = createManagedApiOperation(definition, resource, operation);\n operationConfigs[operationConfig.id] = operationConfig;\n }\n\n return {\n id: resource.name,\n kind: 'database',\n operations: operationConfigs,\n metadata: {\n adapterId: definition.adapter.id,\n collection: resource.collection.name,\n schema: resource.collection.schema ?? null,\n source: 'managed-api',\n },\n };\n}\n\nexport function createManagedApiOperation(\n definition: ManagedApiGenerationDefinition,\n resource: ManagedApiGenerationResourceDefinition,\n operation: ManagedApiCrudOperation,\n): DataOperationConfig {\n const operationId = createManagedApiOperationId(resource.name, operation);\n const primaryKey = resolveManagedApiPrimaryKey(resource.collection);\n const primaryKeyParameter =\n primaryKey === undefined ? undefined : createPrimaryKeyParameter(primaryKey);\n\n return {\n id: operationId,\n endpointId: resource.name,\n name: createManagedApiOperationName(resource.name, operation),\n protocol: 'database',\n intent: mapManagedApiOperationIntent(operation),\n request: createManagedApiOperationRequest(resource.collection, operation, primaryKeyParameter),\n response: createManagedApiOperationResponse(resource.collection, operation),\n metadata: {\n adapterId: definition.adapter.id,\n collection: resource.collection.name,\n operation,\n schema: resource.collection.schema ?? null,\n source: 'managed-api',\n },\n };\n}\n\nexport function getManagedApiOperations(\n resource: ManagedApiGenerationResourceDefinition,\n): readonly ManagedApiCrudOperation[] {\n return resource.operations ?? MANAGED_API_CRUD_OPERATIONS;\n}\n\nexport function createManagedApiOperationId(\n resourceName: string,\n operation: ManagedApiCrudOperation,\n): OperationId {\n return `${resourceName}.${operation}`;\n}\n\nexport function createManagedApiResourceSchema(collection: DbCollectionDefinition): DataSchema {\n const properties: Record<string, DataSchema> = {};\n const required: string[] = [];\n\n for (const field of collection.fields) {\n properties[field.name] = createManagedApiFieldSchema(field);\n if (field.required === true) required.push(field.name);\n }\n\n return {\n type: 'object',\n title: collection.name,\n required: required.length > 0 ? required : undefined,\n properties,\n };\n}\n\nfunction createManagedApiResourceMetadata(\n resource: ManagedApiGenerationResourceDefinition,\n): DataContractValue | undefined {\n if (resource.policies === undefined) return undefined;\n\n return {\n policies: resource.policies.map((policy) => {\n const metadata: Record<string, DataContractValue> = {\n id: policy.id,\n };\n\n if (policy.operation !== undefined) {\n metadata.operation = policy.operation;\n }\n\n return metadata;\n }),\n };\n}\n\nfunction createManagedApiOperationName(\n resourceName: string,\n operation: ManagedApiCrudOperation,\n): string {\n return `${operation} ${resourceName}`;\n}\n\nfunction mapManagedApiOperationIntent(operation: ManagedApiCrudOperation): DataOperationIntent {\n if (operation === 'list' || operation === 'read') return 'read';\n if (operation === 'create') return 'create';\n if (operation === 'update') return 'update';\n return 'delete';\n}\n\nfunction createManagedApiOperationRequest(\n collection: DbCollectionDefinition,\n operation: ManagedApiCrudOperation,\n primaryKeyParameter: DataOperationParameter | undefined,\n): DataOperationConfig['request'] {\n if (operation === 'list') {\n return {\n parameters: [\n {\n name: 'limit',\n location: 'query',\n schema: { type: 'integer' },\n },\n {\n name: 'offset',\n location: 'query',\n schema: { type: 'integer' },\n },\n ],\n };\n }\n\n if (operation === 'read' || operation === 'delete') {\n return primaryKeyParameter === undefined ? undefined : { parameters: [primaryKeyParameter] };\n }\n\n const parameters =\n operation === 'update' && primaryKeyParameter !== undefined ? [primaryKeyParameter] : undefined;\n\n return {\n parameters,\n schema: createManagedApiResourceSchema(collection),\n };\n}\n\nfunction createManagedApiOperationResponse(\n collection: DbCollectionDefinition,\n operation: ManagedApiCrudOperation,\n): DataOperationConfig['response'] {\n const resourceSchema = createManagedApiResourceSchema(collection);\n\n if (operation === 'list') {\n return {\n schema: {\n type: 'array',\n items: resourceSchema,\n },\n };\n }\n\n if (operation === 'delete') {\n return {\n schema: {\n type: 'object',\n properties: {\n deleted: { type: 'boolean' },\n },\n },\n };\n }\n\n return { schema: resourceSchema };\n}\n\nfunction createPrimaryKeyParameter(primaryKey: string): DataOperationParameter {\n return {\n name: primaryKey,\n location: 'path',\n required: true,\n schema: {\n type: 'string',\n },\n };\n}\n\nfunction resolveManagedApiPrimaryKey(collection: DbCollectionDefinition): string | undefined {\n if (collection.primaryKey !== undefined) return collection.primaryKey;\n return collection.fields.find((field) => field.unique === true)?.name;\n}\n\nfunction createManagedApiFieldSchema(field: DbFieldDefinition): DataSchema {\n const schema: DataSchema = mapDbFieldTypeToDataSchema(field.type);\n\n return {\n ...schema,\n default: field.defaultValue,\n };\n}\n\nfunction mapDbFieldTypeToDataSchema(type: DbFieldType): DataSchema {\n if (type === 'boolean') return { type: 'boolean' };\n if (type === 'datetime') return { type: 'string', format: 'date-time' };\n if (type === 'json') return { type: 'object' };\n if (type === 'number') return { type: 'number' };\n if (type === 'uuid') return { type: 'string', format: 'uuid' };\n return { type: 'string' };\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ankhorage/data-sources",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"description": "Headless toolkit for importing, generating, normalizing, and testing Ankhorage data-source configs.",
|
|
6
6
|
"homepage": "https://github.com/ankhorage/data-sources#readme",
|
|
7
7
|
"bugs": {
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createManagedApiDataSource,
|
|
5
|
+
createManagedApiOperationId,
|
|
6
|
+
createManagedApiResourceSchema,
|
|
7
|
+
getManagedApiOperations,
|
|
8
|
+
type ManagedApiGenerationDefinition,
|
|
9
|
+
normalizeManagedApiDataSource,
|
|
10
|
+
} from './index';
|
|
11
|
+
|
|
12
|
+
function assertSerializable<TValue>(value: TValue): void {
|
|
13
|
+
expect(JSON.parse(JSON.stringify(value))).toEqual(value);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function createPostsApiDefinition(): ManagedApiGenerationDefinition {
|
|
17
|
+
return {
|
|
18
|
+
id: 'posts-api',
|
|
19
|
+
name: 'Posts API',
|
|
20
|
+
adapter: {
|
|
21
|
+
id: 'primary-db',
|
|
22
|
+
kind: 'database',
|
|
23
|
+
packageName: '@ankhorage/supabase-db',
|
|
24
|
+
},
|
|
25
|
+
resources: [
|
|
26
|
+
{
|
|
27
|
+
name: 'posts',
|
|
28
|
+
collection: {
|
|
29
|
+
name: 'posts',
|
|
30
|
+
schema: 'public',
|
|
31
|
+
primaryKey: 'id',
|
|
32
|
+
fields: [
|
|
33
|
+
{ name: 'id', type: 'uuid', required: true, unique: true },
|
|
34
|
+
{ name: 'title', type: 'text', required: true },
|
|
35
|
+
{ name: 'published', type: 'boolean', defaultValue: false },
|
|
36
|
+
{ name: 'createdAt', type: 'datetime' },
|
|
37
|
+
{ name: 'metadata', type: 'json' },
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
policies: [{ id: 'posts-policy' }],
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe('managed API generation helpers', () => {
|
|
47
|
+
it('generates deterministic operation IDs', () => {
|
|
48
|
+
expect(createManagedApiOperationId('posts', 'list')).toBe('posts.list');
|
|
49
|
+
expect(createManagedApiOperationId('posts', 'delete')).toBe('posts.delete');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('uses all CRUD operations by default', () => {
|
|
53
|
+
const [resource] = createPostsApiDefinition().resources;
|
|
54
|
+
|
|
55
|
+
expect(resource).toBeDefined();
|
|
56
|
+
if (resource !== undefined) {
|
|
57
|
+
expect(getManagedApiOperations(resource)).toEqual([
|
|
58
|
+
'list',
|
|
59
|
+
'read',
|
|
60
|
+
'create',
|
|
61
|
+
'update',
|
|
62
|
+
'delete',
|
|
63
|
+
]);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('generates a serializable resource schema from a DB collection', () => {
|
|
68
|
+
const [resource] = createPostsApiDefinition().resources;
|
|
69
|
+
|
|
70
|
+
expect(resource).toBeDefined();
|
|
71
|
+
if (resource !== undefined) {
|
|
72
|
+
const schema = createManagedApiResourceSchema(resource.collection);
|
|
73
|
+
|
|
74
|
+
assertSerializable(schema);
|
|
75
|
+
expect(schema.type).toBe('object');
|
|
76
|
+
expect(schema.properties?.id?.format).toBe('uuid');
|
|
77
|
+
expect(schema.properties?.createdAt?.format).toBe('date-time');
|
|
78
|
+
expect(schema.properties?.metadata?.type).toBe('object');
|
|
79
|
+
expect(schema.required).toEqual(['id', 'title']);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('generates list/read/create/update/delete endpoint configs', () => {
|
|
84
|
+
const source = normalizeManagedApiDataSource(createPostsApiDefinition());
|
|
85
|
+
|
|
86
|
+
assertSerializable(source);
|
|
87
|
+
expect(source.kind).toBe('managed-api');
|
|
88
|
+
expect(source.adapter.id).toBe('primary-db');
|
|
89
|
+
expect(source.endpoints.posts?.kind).toBe('database');
|
|
90
|
+
expect(source.endpoints.posts?.operations['posts.list']?.intent).toBe('read');
|
|
91
|
+
expect(source.endpoints.posts?.operations['posts.read']?.intent).toBe('read');
|
|
92
|
+
expect(source.endpoints.posts?.operations['posts.create']?.intent).toBe('create');
|
|
93
|
+
expect(source.endpoints.posts?.operations['posts.update']?.intent).toBe('update');
|
|
94
|
+
expect(source.endpoints.posts?.operations['posts.delete']?.intent).toBe('delete');
|
|
95
|
+
expect(source.endpoints.posts?.operations['posts.read']?.request?.parameters?.[0]?.name).toBe(
|
|
96
|
+
'id',
|
|
97
|
+
);
|
|
98
|
+
expect(
|
|
99
|
+
source.endpoints.posts?.operations['posts.list']?.request?.parameters?.map(
|
|
100
|
+
(parameter) => parameter.name,
|
|
101
|
+
),
|
|
102
|
+
).toEqual(['limit', 'offset']);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('creates diagnostic result for valid managed APIs', () => {
|
|
106
|
+
const result = createManagedApiDataSource(createPostsApiDefinition());
|
|
107
|
+
|
|
108
|
+
expect(result.ok).toBe(true);
|
|
109
|
+
if (result.ok) {
|
|
110
|
+
expect(result.data.resources[0]?.name).toBe('posts');
|
|
111
|
+
expect(result.diagnostics).toEqual([]);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('supports multiple managed APIs backed by different adapter refs', () => {
|
|
116
|
+
const postsApi = normalizeManagedApiDataSource(createPostsApiDefinition());
|
|
117
|
+
const analyticsApi = normalizeManagedApiDataSource({
|
|
118
|
+
id: 'analytics-api',
|
|
119
|
+
adapter: {
|
|
120
|
+
id: 'analytics-db',
|
|
121
|
+
kind: 'database',
|
|
122
|
+
packageName: '@ankhorage/postgres-db',
|
|
123
|
+
},
|
|
124
|
+
resources: [
|
|
125
|
+
{
|
|
126
|
+
name: 'events',
|
|
127
|
+
operations: ['list'],
|
|
128
|
+
collection: {
|
|
129
|
+
name: 'events',
|
|
130
|
+
primaryKey: 'id',
|
|
131
|
+
fields: [
|
|
132
|
+
{ name: 'id', type: 'uuid', required: true, unique: true },
|
|
133
|
+
{ name: 'name', type: 'text', required: true },
|
|
134
|
+
],
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
assertSerializable([postsApi, analyticsApi]);
|
|
141
|
+
expect(postsApi.id).toBe('posts-api');
|
|
142
|
+
expect(analyticsApi.id).toBe('analytics-api');
|
|
143
|
+
expect(postsApi.adapter.id).toBe('primary-db');
|
|
144
|
+
expect(analyticsApi.adapter.id).toBe('analytics-db');
|
|
145
|
+
expect(analyticsApi.endpoints.events?.operations['events.list']?.intent).toBe('read');
|
|
146
|
+
expect(analyticsApi.endpoints.events?.operations['events.read']).toBeUndefined();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('reports missing resources and missing primary key diagnostics', () => {
|
|
150
|
+
const emptyResult = createManagedApiDataSource({
|
|
151
|
+
id: 'empty-api',
|
|
152
|
+
adapter: { id: 'db', kind: 'database' },
|
|
153
|
+
resources: [],
|
|
154
|
+
});
|
|
155
|
+
const missingPrimaryKeyResult = createManagedApiDataSource({
|
|
156
|
+
id: 'broken-api',
|
|
157
|
+
adapter: { id: 'db', kind: 'database' },
|
|
158
|
+
resources: [
|
|
159
|
+
{
|
|
160
|
+
name: 'posts',
|
|
161
|
+
collection: {
|
|
162
|
+
name: 'posts',
|
|
163
|
+
fields: [{ name: 'title', type: 'text' }],
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
expect(emptyResult.ok).toBe(false);
|
|
170
|
+
if (!emptyResult.ok) {
|
|
171
|
+
expect(emptyResult.diagnostics.map((diagnostic) => diagnostic.path)).toContain('resources');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
expect(missingPrimaryKeyResult.ok).toBe(false);
|
|
175
|
+
if (!missingPrimaryKeyResult.ok) {
|
|
176
|
+
expect(missingPrimaryKeyResult.diagnostics.map((diagnostic) => diagnostic.path)).toContain(
|
|
177
|
+
'resources.posts.collection.primaryKey',
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
});
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AdapterRef,
|
|
3
|
+
DataContractValue,
|
|
4
|
+
DataEndpointConfig,
|
|
5
|
+
DataOperationConfig,
|
|
6
|
+
DataOperationIntent,
|
|
7
|
+
DataOperationParameter,
|
|
8
|
+
DataSchema,
|
|
9
|
+
DataSourceDiagnostic,
|
|
10
|
+
DataSourceDiagnosticResult,
|
|
11
|
+
ManagedApiDataSourceConfig,
|
|
12
|
+
ManagedApiResourceConfig,
|
|
13
|
+
OperationId,
|
|
14
|
+
} from '@ankhorage/contracts/data';
|
|
15
|
+
import type {
|
|
16
|
+
DbCollectionDefinition,
|
|
17
|
+
DbFieldDefinition,
|
|
18
|
+
DbFieldType,
|
|
19
|
+
} from '@ankhorage/contracts/db';
|
|
20
|
+
|
|
21
|
+
export const MANAGED_API_CRUD_OPERATIONS = ['list', 'read', 'create', 'update', 'delete'] as const;
|
|
22
|
+
|
|
23
|
+
export type ManagedApiCrudOperation = (typeof MANAGED_API_CRUD_OPERATIONS)[number];
|
|
24
|
+
|
|
25
|
+
export interface ManagedApiOperationPolicyRef {
|
|
26
|
+
readonly id: string;
|
|
27
|
+
readonly operation?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ManagedApiGenerationResourceDefinition {
|
|
31
|
+
readonly name: string;
|
|
32
|
+
readonly collection: DbCollectionDefinition;
|
|
33
|
+
readonly operations?: readonly ManagedApiCrudOperation[];
|
|
34
|
+
readonly policies?: readonly ManagedApiOperationPolicyRef[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ManagedApiGenerationDefinition {
|
|
38
|
+
readonly id: string;
|
|
39
|
+
readonly adapter: AdapterRef;
|
|
40
|
+
readonly name?: string;
|
|
41
|
+
readonly description?: string;
|
|
42
|
+
readonly resources: readonly ManagedApiGenerationResourceDefinition[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function createManagedApiDataSource(
|
|
46
|
+
definition: ManagedApiGenerationDefinition,
|
|
47
|
+
): DataSourceDiagnosticResult<ManagedApiDataSourceConfig> {
|
|
48
|
+
const diagnostics = validateManagedApiDefinition(definition);
|
|
49
|
+
const hasErrors = diagnostics.some((diagnostic) => diagnostic.severity === 'error');
|
|
50
|
+
|
|
51
|
+
if (hasErrors) return { ok: false, diagnostics };
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
ok: true,
|
|
55
|
+
data: normalizeManagedApiDataSource(definition),
|
|
56
|
+
diagnostics,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function validateManagedApiDefinition(
|
|
61
|
+
definition: ManagedApiGenerationDefinition,
|
|
62
|
+
): readonly DataSourceDiagnostic[] {
|
|
63
|
+
const diagnostics: DataSourceDiagnostic[] = [];
|
|
64
|
+
|
|
65
|
+
if (definition.resources.length === 0) {
|
|
66
|
+
diagnostics.push({
|
|
67
|
+
code: 'invalid-config',
|
|
68
|
+
dataSourceId: definition.id,
|
|
69
|
+
message: 'Managed API generation requires at least one resource.',
|
|
70
|
+
path: 'resources',
|
|
71
|
+
severity: 'error',
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
for (const resource of definition.resources) {
|
|
76
|
+
if (resource.name.trim().length === 0) {
|
|
77
|
+
diagnostics.push({
|
|
78
|
+
code: 'invalid-config',
|
|
79
|
+
dataSourceId: definition.id,
|
|
80
|
+
endpointId: resource.name,
|
|
81
|
+
message: 'Managed API resource names must be non-empty.',
|
|
82
|
+
path: 'resources.name',
|
|
83
|
+
severity: 'error',
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (resource.collection.name.trim().length === 0) {
|
|
88
|
+
diagnostics.push({
|
|
89
|
+
code: 'invalid-config',
|
|
90
|
+
dataSourceId: definition.id,
|
|
91
|
+
endpointId: resource.name,
|
|
92
|
+
message: 'Managed API resource collections must have a non-empty name.',
|
|
93
|
+
path: `resources.${resource.name}.collection.name`,
|
|
94
|
+
severity: 'error',
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const primaryKey = resolveManagedApiPrimaryKey(resource.collection);
|
|
99
|
+
if (
|
|
100
|
+
primaryKey === undefined &&
|
|
101
|
+
getManagedApiOperations(resource).some((operation) => operation !== 'list')
|
|
102
|
+
) {
|
|
103
|
+
diagnostics.push({
|
|
104
|
+
code: 'invalid-config',
|
|
105
|
+
dataSourceId: definition.id,
|
|
106
|
+
endpointId: resource.name,
|
|
107
|
+
message:
|
|
108
|
+
'Managed API resources need a primaryKey field when generating read/create/update/delete operations.',
|
|
109
|
+
path: `resources.${resource.name}.collection.primaryKey`,
|
|
110
|
+
severity: 'error',
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return diagnostics;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function normalizeManagedApiDataSource(
|
|
119
|
+
definition: ManagedApiGenerationDefinition,
|
|
120
|
+
): ManagedApiDataSourceConfig {
|
|
121
|
+
const resources: ManagedApiResourceConfig[] = [];
|
|
122
|
+
const endpoints: Record<string, DataEndpointConfig> = {};
|
|
123
|
+
const schemas: Record<string, DataSchema> = {};
|
|
124
|
+
|
|
125
|
+
for (const resource of definition.resources) {
|
|
126
|
+
const operations = getManagedApiOperations(resource);
|
|
127
|
+
const resourceConfig: ManagedApiResourceConfig = {
|
|
128
|
+
name: resource.name,
|
|
129
|
+
collection: resource.collection,
|
|
130
|
+
operations,
|
|
131
|
+
metadata: createManagedApiResourceMetadata(resource),
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
resources.push(resourceConfig);
|
|
135
|
+
endpoints[resource.name] = createManagedApiEndpoint(definition, resource, operations);
|
|
136
|
+
schemas[resource.name] = createManagedApiResourceSchema(resource.collection);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
id: definition.id,
|
|
141
|
+
kind: 'managed-api',
|
|
142
|
+
name: definition.name,
|
|
143
|
+
description: definition.description,
|
|
144
|
+
adapter: definition.adapter,
|
|
145
|
+
resources,
|
|
146
|
+
endpoints,
|
|
147
|
+
schemas,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function createManagedApiEndpoint(
|
|
152
|
+
definition: ManagedApiGenerationDefinition,
|
|
153
|
+
resource: ManagedApiGenerationResourceDefinition,
|
|
154
|
+
operations: readonly ManagedApiCrudOperation[] = getManagedApiOperations(resource),
|
|
155
|
+
): DataEndpointConfig {
|
|
156
|
+
const operationConfigs: Record<OperationId, DataOperationConfig> = {};
|
|
157
|
+
|
|
158
|
+
for (const operation of operations) {
|
|
159
|
+
const operationConfig = createManagedApiOperation(definition, resource, operation);
|
|
160
|
+
operationConfigs[operationConfig.id] = operationConfig;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
id: resource.name,
|
|
165
|
+
kind: 'database',
|
|
166
|
+
operations: operationConfigs,
|
|
167
|
+
metadata: {
|
|
168
|
+
adapterId: definition.adapter.id,
|
|
169
|
+
collection: resource.collection.name,
|
|
170
|
+
schema: resource.collection.schema ?? null,
|
|
171
|
+
source: 'managed-api',
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function createManagedApiOperation(
|
|
177
|
+
definition: ManagedApiGenerationDefinition,
|
|
178
|
+
resource: ManagedApiGenerationResourceDefinition,
|
|
179
|
+
operation: ManagedApiCrudOperation,
|
|
180
|
+
): DataOperationConfig {
|
|
181
|
+
const operationId = createManagedApiOperationId(resource.name, operation);
|
|
182
|
+
const primaryKey = resolveManagedApiPrimaryKey(resource.collection);
|
|
183
|
+
const primaryKeyParameter =
|
|
184
|
+
primaryKey === undefined ? undefined : createPrimaryKeyParameter(primaryKey);
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
id: operationId,
|
|
188
|
+
endpointId: resource.name,
|
|
189
|
+
name: createManagedApiOperationName(resource.name, operation),
|
|
190
|
+
protocol: 'database',
|
|
191
|
+
intent: mapManagedApiOperationIntent(operation),
|
|
192
|
+
request: createManagedApiOperationRequest(resource.collection, operation, primaryKeyParameter),
|
|
193
|
+
response: createManagedApiOperationResponse(resource.collection, operation),
|
|
194
|
+
metadata: {
|
|
195
|
+
adapterId: definition.adapter.id,
|
|
196
|
+
collection: resource.collection.name,
|
|
197
|
+
operation,
|
|
198
|
+
schema: resource.collection.schema ?? null,
|
|
199
|
+
source: 'managed-api',
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function getManagedApiOperations(
|
|
205
|
+
resource: ManagedApiGenerationResourceDefinition,
|
|
206
|
+
): readonly ManagedApiCrudOperation[] {
|
|
207
|
+
return resource.operations ?? MANAGED_API_CRUD_OPERATIONS;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function createManagedApiOperationId(
|
|
211
|
+
resourceName: string,
|
|
212
|
+
operation: ManagedApiCrudOperation,
|
|
213
|
+
): OperationId {
|
|
214
|
+
return `${resourceName}.${operation}`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function createManagedApiResourceSchema(collection: DbCollectionDefinition): DataSchema {
|
|
218
|
+
const properties: Record<string, DataSchema> = {};
|
|
219
|
+
const required: string[] = [];
|
|
220
|
+
|
|
221
|
+
for (const field of collection.fields) {
|
|
222
|
+
properties[field.name] = createManagedApiFieldSchema(field);
|
|
223
|
+
if (field.required === true) required.push(field.name);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
type: 'object',
|
|
228
|
+
title: collection.name,
|
|
229
|
+
required: required.length > 0 ? required : undefined,
|
|
230
|
+
properties,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function createManagedApiResourceMetadata(
|
|
235
|
+
resource: ManagedApiGenerationResourceDefinition,
|
|
236
|
+
): DataContractValue | undefined {
|
|
237
|
+
if (resource.policies === undefined) return undefined;
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
policies: resource.policies.map((policy) => {
|
|
241
|
+
const metadata: Record<string, DataContractValue> = {
|
|
242
|
+
id: policy.id,
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
if (policy.operation !== undefined) {
|
|
246
|
+
metadata.operation = policy.operation;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return metadata;
|
|
250
|
+
}),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function createManagedApiOperationName(
|
|
255
|
+
resourceName: string,
|
|
256
|
+
operation: ManagedApiCrudOperation,
|
|
257
|
+
): string {
|
|
258
|
+
return `${operation} ${resourceName}`;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function mapManagedApiOperationIntent(operation: ManagedApiCrudOperation): DataOperationIntent {
|
|
262
|
+
if (operation === 'list' || operation === 'read') return 'read';
|
|
263
|
+
if (operation === 'create') return 'create';
|
|
264
|
+
if (operation === 'update') return 'update';
|
|
265
|
+
return 'delete';
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function createManagedApiOperationRequest(
|
|
269
|
+
collection: DbCollectionDefinition,
|
|
270
|
+
operation: ManagedApiCrudOperation,
|
|
271
|
+
primaryKeyParameter: DataOperationParameter | undefined,
|
|
272
|
+
): DataOperationConfig['request'] {
|
|
273
|
+
if (operation === 'list') {
|
|
274
|
+
return {
|
|
275
|
+
parameters: [
|
|
276
|
+
{
|
|
277
|
+
name: 'limit',
|
|
278
|
+
location: 'query',
|
|
279
|
+
schema: { type: 'integer' },
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
name: 'offset',
|
|
283
|
+
location: 'query',
|
|
284
|
+
schema: { type: 'integer' },
|
|
285
|
+
},
|
|
286
|
+
],
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (operation === 'read' || operation === 'delete') {
|
|
291
|
+
return primaryKeyParameter === undefined ? undefined : { parameters: [primaryKeyParameter] };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const parameters =
|
|
295
|
+
operation === 'update' && primaryKeyParameter !== undefined ? [primaryKeyParameter] : undefined;
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
parameters,
|
|
299
|
+
schema: createManagedApiResourceSchema(collection),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function createManagedApiOperationResponse(
|
|
304
|
+
collection: DbCollectionDefinition,
|
|
305
|
+
operation: ManagedApiCrudOperation,
|
|
306
|
+
): DataOperationConfig['response'] {
|
|
307
|
+
const resourceSchema = createManagedApiResourceSchema(collection);
|
|
308
|
+
|
|
309
|
+
if (operation === 'list') {
|
|
310
|
+
return {
|
|
311
|
+
schema: {
|
|
312
|
+
type: 'array',
|
|
313
|
+
items: resourceSchema,
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (operation === 'delete') {
|
|
319
|
+
return {
|
|
320
|
+
schema: {
|
|
321
|
+
type: 'object',
|
|
322
|
+
properties: {
|
|
323
|
+
deleted: { type: 'boolean' },
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return { schema: resourceSchema };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function createPrimaryKeyParameter(primaryKey: string): DataOperationParameter {
|
|
333
|
+
return {
|
|
334
|
+
name: primaryKey,
|
|
335
|
+
location: 'path',
|
|
336
|
+
required: true,
|
|
337
|
+
schema: {
|
|
338
|
+
type: 'string',
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function resolveManagedApiPrimaryKey(collection: DbCollectionDefinition): string | undefined {
|
|
344
|
+
if (collection.primaryKey !== undefined) return collection.primaryKey;
|
|
345
|
+
return collection.fields.find((field) => field.unique === true)?.name;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function createManagedApiFieldSchema(field: DbFieldDefinition): DataSchema {
|
|
349
|
+
const schema: DataSchema = mapDbFieldTypeToDataSchema(field.type);
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
...schema,
|
|
353
|
+
default: field.defaultValue,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function mapDbFieldTypeToDataSchema(type: DbFieldType): DataSchema {
|
|
358
|
+
if (type === 'boolean') return { type: 'boolean' };
|
|
359
|
+
if (type === 'datetime') return { type: 'string', format: 'date-time' };
|
|
360
|
+
if (type === 'json') return { type: 'object' };
|
|
361
|
+
if (type === 'number') return { type: 'number' };
|
|
362
|
+
if (type === 'uuid') return { type: 'string', format: 'uuid' };
|
|
363
|
+
return { type: 'string' };
|
|
364
|
+
}
|