@rangka/core 0.1.0 → 0.1.2
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/package.json +6 -2
- package/.claude/skills/extend-core/SKILL.md +0 -133
- package/.turbo/turbo-build.log +0 -4
- package/CHANGELOG.md +0 -18
- package/CLAUDE.md +0 -180
- package/src/__tests__/coerce.test.ts +0 -154
- package/src/__tests__/context.test.ts +0 -111
- package/src/__tests__/helpers.ts +0 -21
- package/src/__tests__/index.test.ts +0 -7
- package/src/__tests__/widgets.test.ts +0 -197
- package/src/api/__tests__/handlers.test.ts +0 -389
- package/src/api/__tests__/include-resolver.test.ts +0 -393
- package/src/api/__tests__/middleware.test.ts +0 -100
- package/src/api/__tests__/openapi-schema.test.ts +0 -210
- package/src/api/__tests__/query-parser.test.ts +0 -291
- package/src/api/__tests__/route-generator.test.ts +0 -137
- package/src/api/__tests__/server.test.ts +0 -73
- package/src/api/__tests__/swagger.test.ts +0 -166
- package/src/api/handlers.ts +0 -274
- package/src/api/include-resolver.ts +0 -27
- package/src/api/index.ts +0 -4
- package/src/api/meta-handler.ts +0 -254
- package/src/api/openapi-schema.ts +0 -99
- package/src/api/query-parser.ts +0 -315
- package/src/api/route-generator.ts +0 -448
- package/src/api/server.ts +0 -147
- package/src/api/types.ts +0 -16
- package/src/audit/__tests__/audit.test.ts +0 -144
- package/src/audit/index.ts +0 -3
- package/src/audit/record.ts +0 -69
- package/src/audit/tables.ts +0 -48
- package/src/audit/types.ts +0 -26
- package/src/auth/__tests__/core-module.test.ts +0 -54
- package/src/auth/__tests__/debug.test.ts +0 -47
- package/src/auth/__tests__/field-permissions.test.ts +0 -245
- package/src/auth/__tests__/integration.test.ts +0 -208
- package/src/auth/__tests__/meta-boot.test.ts +0 -538
- package/src/auth/__tests__/model-permissions.test.ts +0 -205
- package/src/auth/__tests__/password.test.ts +0 -29
- package/src/auth/__tests__/permission-registry.test.ts +0 -313
- package/src/auth/__tests__/scope-hook.test.ts +0 -509
- package/src/auth/__tests__/scope-registry.test.ts +0 -297
- package/src/auth/__tests__/scopes.test.ts +0 -66
- package/src/auth/__tests__/session.test.ts +0 -214
- package/src/auth/core-models.ts +0 -52
- package/src/auth/core-module.ts +0 -59
- package/src/auth/debug.ts +0 -157
- package/src/auth/field-permissions.ts +0 -116
- package/src/auth/index.ts +0 -37
- package/src/auth/model-permissions.ts +0 -59
- package/src/auth/password.ts +0 -22
- package/src/auth/permission-registry.ts +0 -171
- package/src/auth/scope-filters.ts +0 -11
- package/src/auth/scope-registry.ts +0 -121
- package/src/auth/scopes.ts +0 -146
- package/src/auth/seed.ts +0 -44
- package/src/auth/session.ts +0 -178
- package/src/auth/types.ts +0 -50
- package/src/boot/__tests__/page-scanning.test.ts +0 -170
- package/src/boot/__tests__/page-utils.test.ts +0 -225
- package/src/boot/__tests__/project-scanner.test.ts +0 -88
- package/src/boot/dependency-sort.ts +0 -82
- package/src/boot/discovery.ts +0 -85
- package/src/boot/index.ts +0 -457
- package/src/boot/page-utils.ts +0 -110
- package/src/boot/project-scanner.ts +0 -397
- package/src/boot/schema-loader.ts +0 -26
- package/src/boot/schema-merger.ts +0 -125
- package/src/boot/traits.ts +0 -25
- package/src/boot/types.ts +0 -73
- package/src/context.ts +0 -105
- package/src/db/__tests__/cascade-delete.test.ts +0 -182
- package/src/db/__tests__/desired-state.test.ts +0 -136
- package/src/db/__tests__/diff-engine.test.ts +0 -635
- package/src/db/__tests__/field-mapper.test.ts +0 -355
- package/src/db/__tests__/introspect.test.ts +0 -70
- package/src/db/__tests__/search-filter.test.ts +0 -45
- package/src/db/__tests__/sequence.test.ts +0 -221
- package/src/db/auto-sync.ts +0 -133
- package/src/db/client.ts +0 -147
- package/src/db/desired-state.ts +0 -98
- package/src/db/diff-engine.ts +0 -305
- package/src/db/field-mapper.ts +0 -504
- package/src/db/filter-applier.ts +0 -89
- package/src/db/include-resolver.ts +0 -40
- package/src/db/index.ts +0 -23
- package/src/db/introspect.ts +0 -265
- package/src/db/model-include-resolver.ts +0 -327
- package/src/db/model-ops.ts +0 -281
- package/src/db/scope-enforcer.ts +0 -37
- package/src/db/types.ts +0 -98
- package/src/errors.ts +0 -41
- package/src/events/__tests__/bus.test.ts +0 -105
- package/src/events/bus.ts +0 -89
- package/src/events/index.ts +0 -2
- package/src/events/types.ts +0 -9
- package/src/external-model/__tests__/computed-fields.test.ts +0 -106
- package/src/external-model/__tests__/field-mapper.test.ts +0 -160
- package/src/external-model/__tests__/in-memory-ops.test.ts +0 -247
- package/src/external-model/__tests__/mutation-executor.test.ts +0 -160
- package/src/external-model/__tests__/query-executor.test.ts +0 -284
- package/src/external-model/__tests__/schema-converter.test.ts +0 -174
- package/src/external-model/computed-fields.ts +0 -15
- package/src/external-model/define.ts +0 -5
- package/src/external-model/external-model-ops.ts +0 -108
- package/src/external-model/field-mapper.ts +0 -66
- package/src/external-model/in-memory-ops.ts +0 -107
- package/src/external-model/index.ts +0 -7
- package/src/external-model/mutation-executor.ts +0 -71
- package/src/external-model/query-executor.ts +0 -100
- package/src/external-model/schema-converter.ts +0 -53
- package/src/external-model/types.ts +0 -32
- package/src/fixtures/__tests__/fixtures.test.ts +0 -203
- package/src/fixtures/index.ts +0 -10
- package/src/fixtures/loader.ts +0 -196
- package/src/fixtures/registry.ts +0 -125
- package/src/fixtures/types.ts +0 -33
- package/src/helpers/assert-ownership.ts +0 -19
- package/src/helpers/coerce.ts +0 -28
- package/src/helpers/stamping.ts +0 -28
- package/src/helpers/validation.ts +0 -14
- package/src/hooks/__tests__/context.test.ts +0 -73
- package/src/hooks/__tests__/executor.test.ts +0 -433
- package/src/hooks/__tests__/middleware.test.ts +0 -224
- package/src/hooks/__tests__/registry.test.ts +0 -50
- package/src/hooks/context.ts +0 -89
- package/src/hooks/errors.ts +0 -11
- package/src/hooks/executor.ts +0 -115
- package/src/hooks/index.ts +0 -10
- package/src/hooks/middleware.ts +0 -220
- package/src/hooks/registry.ts +0 -20
- package/src/hooks/types.ts +0 -32
- package/src/index.ts +0 -172
- package/src/jobs/__tests__/enqueue.test.ts +0 -77
- package/src/jobs/__tests__/integration.test.ts +0 -71
- package/src/jobs/__tests__/registry.test.ts +0 -103
- package/src/jobs/__tests__/scheduler.test.ts +0 -92
- package/src/jobs/__tests__/worker-execution.test.ts +0 -202
- package/src/jobs/__tests__/worker.test.ts +0 -119
- package/src/jobs/enqueue.ts +0 -93
- package/src/jobs/index.ts +0 -14
- package/src/jobs/registry.ts +0 -92
- package/src/jobs/scheduler.ts +0 -205
- package/src/jobs/tables.ts +0 -132
- package/src/jobs/types.ts +0 -62
- package/src/jobs/worker.ts +0 -272
- package/src/model-api/__tests__/cross-boundary-includes.test.ts +0 -366
- package/src/model-api/__tests__/extended-api.test.ts +0 -244
- package/src/model-api/__tests__/filter-applier.test.ts +0 -177
- package/src/model-api/__tests__/filter-translator.test.ts +0 -186
- package/src/model-api/__tests__/include-resolver.test.ts +0 -226
- package/src/model-api/__tests__/model-access.test.ts +0 -284
- package/src/model-api/__tests__/query-builder.test.ts +0 -224
- package/src/model-api/__tests__/scope-enforcer.test.ts +0 -268
- package/src/model-api/field-access.ts +0 -28
- package/src/model-api/filter-applier.ts +0 -1
- package/src/model-api/filter-translator.ts +0 -67
- package/src/model-api/include-resolver.ts +0 -2
- package/src/model-api/index.ts +0 -86
- package/src/model-api/query-builder.ts +0 -155
- package/src/model-api/scope-enforcer.ts +0 -3
- package/src/model-api/types.ts +0 -139
- package/src/plugins/__tests__/adapter-registry.test.ts +0 -92
- package/src/plugins/__tests__/lifecycle.test.ts +0 -96
- package/src/plugins/__tests__/loader.test.ts +0 -273
- package/src/plugins/__tests__/validator.test.ts +0 -275
- package/src/plugins/adapter-registry.ts +0 -42
- package/src/plugins/define.ts +0 -5
- package/src/plugins/index.ts +0 -28
- package/src/plugins/lifecycle.ts +0 -27
- package/src/plugins/loader.ts +0 -126
- package/src/plugins/types.ts +0 -76
- package/src/plugins/validator.ts +0 -141
- package/src/schema/__tests__/registry-models-by-module.test.ts +0 -58
- package/src/schema/registry.ts +0 -93
- package/src/schema/relationships.ts +0 -93
- package/src/schema/types.ts +0 -43
- package/src/services/__tests__/integration.test.ts +0 -63
- package/src/services/__tests__/registry.test.ts +0 -175
- package/src/services/index.ts +0 -13
- package/src/services/registry.ts +0 -156
- package/src/services/types.ts +0 -27
- package/src/validation/__tests__/field-validator.test.ts +0 -195
- package/src/validation/field-validator.ts +0 -113
- package/src/validation/index.ts +0 -1
- package/src/widgets/index.ts +0 -3
- package/src/widgets/slot-validator.ts +0 -87
- package/src/widgets/widget-registry.ts +0 -32
- package/tests/boot.test.ts +0 -323
- package/tests/dependency-sort.test.ts +0 -99
- package/tests/discovery.test.ts +0 -126
- package/tests/registry.test.ts +0 -216
- package/tests/schema-loader.test.ts +0 -52
- package/tests/schema-merger.test.ts +0 -180
- package/tsconfig.json +0 -9
- package/tsconfig.tsbuildinfo +0 -1
- package/vitest.config.ts +0 -14
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import type { TranslatedFilter } from '../model-api/filter-translator.js';
|
|
2
|
-
import { toBool, isNil } from '../helpers/coerce.js';
|
|
3
|
-
|
|
4
|
-
export function applyInMemoryFilters(
|
|
5
|
-
records: Record<string, unknown>[],
|
|
6
|
-
filters: TranslatedFilter[],
|
|
7
|
-
): Record<string, unknown>[] {
|
|
8
|
-
return records.filter((record) => {
|
|
9
|
-
for (const { field, operator, value } of filters) {
|
|
10
|
-
const fieldValue = record[field];
|
|
11
|
-
|
|
12
|
-
switch (operator) {
|
|
13
|
-
case 'eq':
|
|
14
|
-
if (fieldValue !== value) return false;
|
|
15
|
-
break;
|
|
16
|
-
case 'neq':
|
|
17
|
-
if (fieldValue === value) return false;
|
|
18
|
-
break;
|
|
19
|
-
case 'gt':
|
|
20
|
-
if (!(fieldValue! > value!)) return false;
|
|
21
|
-
break;
|
|
22
|
-
case 'gte':
|
|
23
|
-
if (!(fieldValue! >= value!)) return false;
|
|
24
|
-
break;
|
|
25
|
-
case 'lt':
|
|
26
|
-
if (!(fieldValue! < value!)) return false;
|
|
27
|
-
break;
|
|
28
|
-
case 'lte':
|
|
29
|
-
if (!(fieldValue! <= value!)) return false;
|
|
30
|
-
break;
|
|
31
|
-
case 'in':
|
|
32
|
-
if (!(value as unknown[]).includes(fieldValue)) return false;
|
|
33
|
-
break;
|
|
34
|
-
case 'notIn':
|
|
35
|
-
if ((value as unknown[]).includes(fieldValue)) return false;
|
|
36
|
-
break;
|
|
37
|
-
case 'contains':
|
|
38
|
-
if (
|
|
39
|
-
typeof fieldValue !== 'string' ||
|
|
40
|
-
!fieldValue.toLowerCase().includes((value as string).toLowerCase())
|
|
41
|
-
)
|
|
42
|
-
return false;
|
|
43
|
-
break;
|
|
44
|
-
case 'startsWith':
|
|
45
|
-
if (
|
|
46
|
-
typeof fieldValue !== 'string' ||
|
|
47
|
-
!fieldValue.toLowerCase().startsWith((value as string).toLowerCase())
|
|
48
|
-
)
|
|
49
|
-
return false;
|
|
50
|
-
break;
|
|
51
|
-
case 'endsWith':
|
|
52
|
-
if (
|
|
53
|
-
typeof fieldValue !== 'string' ||
|
|
54
|
-
!fieldValue.toLowerCase().endsWith((value as string).toLowerCase())
|
|
55
|
-
)
|
|
56
|
-
return false;
|
|
57
|
-
break;
|
|
58
|
-
case 'is':
|
|
59
|
-
case 'isnull':
|
|
60
|
-
if (isNil(value) || toBool(value)) {
|
|
61
|
-
if (!isNil(fieldValue)) return false;
|
|
62
|
-
} else {
|
|
63
|
-
if (isNil(fieldValue)) return false;
|
|
64
|
-
}
|
|
65
|
-
break;
|
|
66
|
-
case 'like':
|
|
67
|
-
if (
|
|
68
|
-
typeof fieldValue !== 'string' ||
|
|
69
|
-
!fieldValue.toLowerCase().includes((value as string).toLowerCase())
|
|
70
|
-
)
|
|
71
|
-
return false;
|
|
72
|
-
break;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return true;
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export function applyInMemorySort(
|
|
80
|
-
records: Record<string, unknown>[],
|
|
81
|
-
sorts: Array<{ field: string; direction: 'asc' | 'desc' }>,
|
|
82
|
-
): Record<string, unknown>[] {
|
|
83
|
-
if (sorts.length === 0) return records;
|
|
84
|
-
|
|
85
|
-
return [...records].sort((a, b) => {
|
|
86
|
-
for (const { field, direction } of sorts) {
|
|
87
|
-
const aVal = a[field];
|
|
88
|
-
const bVal = b[field];
|
|
89
|
-
|
|
90
|
-
if (aVal === bVal) continue;
|
|
91
|
-
if (aVal == null) return direction === 'asc' ? -1 : 1;
|
|
92
|
-
if (bVal == null) return direction === 'asc' ? 1 : -1;
|
|
93
|
-
|
|
94
|
-
const cmp = aVal < bVal ? -1 : 1;
|
|
95
|
-
return direction === 'asc' ? cmp : -cmp;
|
|
96
|
-
}
|
|
97
|
-
return 0;
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export function applyInMemoryPagination(
|
|
102
|
-
records: Record<string, unknown>[],
|
|
103
|
-
limit: number,
|
|
104
|
-
offset: number,
|
|
105
|
-
): Record<string, unknown>[] {
|
|
106
|
-
return records.slice(offset, offset + limit);
|
|
107
|
-
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
export { defineExternalModel } from './define.js';
|
|
2
|
-
export { mapAdapterResponse, reverseMapForWrite, resolveFieldValue } from './field-mapper.js';
|
|
3
|
-
export { evaluateComputedFields } from './computed-fields.js';
|
|
4
|
-
export { externalModelToResolved } from './schema-converter.js';
|
|
5
|
-
export type { ExternalModelConfig, ExternalFieldConfig, ComputedFieldConfig } from './types.js';
|
|
6
|
-
export { ExternalModelOps } from './external-model-ops.js';
|
|
7
|
-
export type { ExternalModelOpsConfig } from './external-model-ops.js';
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import type { DataAdapter, AdapterCapability } from '../plugins/types.js';
|
|
2
|
-
import type { ExternalFieldConfig } from './types.js';
|
|
3
|
-
import { reverseMapForWrite, mapAdapterResponse } from './field-mapper.js';
|
|
4
|
-
import { evaluateComputedFields } from './computed-fields.js';
|
|
5
|
-
|
|
6
|
-
export class CapabilityNotSupportedError extends Error {
|
|
7
|
-
constructor(
|
|
8
|
-
public readonly adapterName: string,
|
|
9
|
-
public readonly operation: string,
|
|
10
|
-
) {
|
|
11
|
-
super(`Adapter "${adapterName}" does not support operation "${operation}"`);
|
|
12
|
-
this.name = 'CapabilityNotSupportedError';
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface ExternalMutationOptions {
|
|
17
|
-
adapter: DataAdapter;
|
|
18
|
-
adapterName: string;
|
|
19
|
-
modelName: string;
|
|
20
|
-
fields: Record<string, ExternalFieldConfig>;
|
|
21
|
-
capabilities: AdapterCapability[];
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export class ExternalMutationExecutor {
|
|
25
|
-
private readonly adapter: DataAdapter;
|
|
26
|
-
private readonly adapterName: string;
|
|
27
|
-
private readonly modelName: string;
|
|
28
|
-
private readonly fields: Record<string, ExternalFieldConfig>;
|
|
29
|
-
private readonly capabilities: Set<AdapterCapability>;
|
|
30
|
-
|
|
31
|
-
constructor(options: ExternalMutationOptions) {
|
|
32
|
-
this.adapter = options.adapter;
|
|
33
|
-
this.adapterName = options.adapterName;
|
|
34
|
-
this.modelName = options.modelName;
|
|
35
|
-
this.fields = options.fields;
|
|
36
|
-
this.capabilities = new Set(options.capabilities);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async create(data: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
40
|
-
if (!this.capabilities.has('create') || !this.adapter.create) {
|
|
41
|
-
throw new CapabilityNotSupportedError(this.adapterName, 'create');
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const mapped = reverseMapForWrite(data, this.fields);
|
|
45
|
-
const raw = await this.adapter.create(this.modelName, mapped);
|
|
46
|
-
return this.transformRecord(raw);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async update(id: string, data: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
50
|
-
if (!this.capabilities.has('update') || !this.adapter.update) {
|
|
51
|
-
throw new CapabilityNotSupportedError(this.adapterName, 'update');
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const mapped = reverseMapForWrite(data, this.fields);
|
|
55
|
-
const raw = await this.adapter.update(this.modelName, id, mapped);
|
|
56
|
-
return this.transformRecord(raw);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async delete(id: string): Promise<void> {
|
|
60
|
-
if (!this.capabilities.has('delete') || !this.adapter.delete) {
|
|
61
|
-
throw new CapabilityNotSupportedError(this.adapterName, 'delete');
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
await this.adapter.delete(this.modelName, id);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
private transformRecord(raw: Record<string, unknown>): Record<string, unknown> {
|
|
68
|
-
const mapped = mapAdapterResponse(raw, this.fields);
|
|
69
|
-
return evaluateComputedFields(mapped, this.fields);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import type { DataAdapter, ListQuery } from '../plugins/types.js';
|
|
2
|
-
import type { ExternalFieldConfig } from './types.js';
|
|
3
|
-
import type { TranslatedFilter } from '../model-api/filter-translator.js';
|
|
4
|
-
import { mapAdapterResponse } from './field-mapper.js';
|
|
5
|
-
import { evaluateComputedFields } from './computed-fields.js';
|
|
6
|
-
import {
|
|
7
|
-
applyInMemoryFilters,
|
|
8
|
-
applyInMemorySort,
|
|
9
|
-
applyInMemoryPagination,
|
|
10
|
-
} from './in-memory-ops.js';
|
|
11
|
-
import type { AdapterCapability } from '../plugins/types.js';
|
|
12
|
-
|
|
13
|
-
export interface ExternalQueryOptions {
|
|
14
|
-
adapter: DataAdapter;
|
|
15
|
-
modelName: string;
|
|
16
|
-
fields: Record<string, ExternalFieldConfig>;
|
|
17
|
-
capabilities: AdapterCapability[];
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface ExternalQueryState {
|
|
21
|
-
filters: TranslatedFilter[];
|
|
22
|
-
sorts: Array<{ field: string; direction: 'asc' | 'desc' }>;
|
|
23
|
-
limitVal?: number;
|
|
24
|
-
offsetVal?: number;
|
|
25
|
-
pageVal?: number;
|
|
26
|
-
fieldNames: string[];
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export class ExternalQueryExecutor {
|
|
30
|
-
private readonly adapter: DataAdapter;
|
|
31
|
-
private readonly modelName: string;
|
|
32
|
-
private readonly fields: Record<string, ExternalFieldConfig>;
|
|
33
|
-
private readonly capabilities: Set<AdapterCapability>;
|
|
34
|
-
|
|
35
|
-
constructor(options: ExternalQueryOptions) {
|
|
36
|
-
this.adapter = options.adapter;
|
|
37
|
-
this.modelName = options.modelName;
|
|
38
|
-
this.fields = options.fields;
|
|
39
|
-
this.capabilities = new Set(options.capabilities);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
async execGet(id: string): Promise<Record<string, unknown> | null> {
|
|
43
|
-
const raw = await this.adapter.get(this.modelName, id);
|
|
44
|
-
if (!raw) return null;
|
|
45
|
-
return this.transformRecord(raw);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async execList(
|
|
49
|
-
state: ExternalQueryState,
|
|
50
|
-
): Promise<{ data: Record<string, unknown>[]; total?: number; hasMore?: boolean }> {
|
|
51
|
-
const limit = state.limitVal ?? 25;
|
|
52
|
-
const page = state.pageVal ?? 1;
|
|
53
|
-
|
|
54
|
-
if (this.capabilities.has('list')) {
|
|
55
|
-
const query: ListQuery = { pageSize: limit, page };
|
|
56
|
-
|
|
57
|
-
if (state.filters.length > 0 && this.capabilities.has('filter')) {
|
|
58
|
-
query.filters = state.filters.map((f) => ({
|
|
59
|
-
field: f.field,
|
|
60
|
-
operator: f.operator,
|
|
61
|
-
value: f.value,
|
|
62
|
-
}));
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (state.sorts.length > 0 && this.capabilities.has('sort')) {
|
|
66
|
-
query.sort = state.sorts[0];
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const result = await this.adapter.list!(this.modelName, query);
|
|
70
|
-
let data = result.data.map((r) => this.transformRecord(r));
|
|
71
|
-
|
|
72
|
-
if (state.filters.length > 0 && !this.capabilities.has('filter')) {
|
|
73
|
-
data = applyInMemoryFilters(data, state.filters);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (state.sorts.length > 0 && !this.capabilities.has('sort')) {
|
|
77
|
-
data = applyInMemorySort(data, state.sorts);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (!this.capabilities.has('filter') || !this.capabilities.has('sort')) {
|
|
81
|
-
data = applyInMemoryPagination(data, limit, 0);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return { data, total: result.total, hasMore: result.hasMore };
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Fallback: no list capability, fetch all via repeated get (not practical for real use)
|
|
88
|
-
return { data: [], total: 0 };
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
async execCount(state: ExternalQueryState): Promise<number> {
|
|
92
|
-
const result = await this.execList(state);
|
|
93
|
-
return result.total ?? result.data.length;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
private transformRecord(raw: Record<string, unknown>): Record<string, unknown> {
|
|
97
|
-
const mapped = mapAdapterResponse(raw, this.fields);
|
|
98
|
-
return evaluateComputedFields(mapped, this.fields);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import type { ResolvedModel, ResolvedField, ModelRelationship } from '../schema/types.js';
|
|
2
|
-
import type { ExternalModelConfig } from './types.js';
|
|
3
|
-
|
|
4
|
-
export interface ExternalModelConversionResult {
|
|
5
|
-
model: ResolvedModel;
|
|
6
|
-
relationships: ModelRelationship[];
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function externalModelToResolved(
|
|
10
|
-
config: ExternalModelConfig,
|
|
11
|
-
app: string,
|
|
12
|
-
module: string,
|
|
13
|
-
): ExternalModelConversionResult {
|
|
14
|
-
const qualifiedName = `${module}.${config.name}`;
|
|
15
|
-
|
|
16
|
-
const fields: ResolvedField[] = Object.entries(config.fields).map(([name, fieldConfig]) => ({
|
|
17
|
-
name,
|
|
18
|
-
config: {
|
|
19
|
-
type: fieldConfig.type,
|
|
20
|
-
label: fieldConfig.label,
|
|
21
|
-
required: fieldConfig.required,
|
|
22
|
-
},
|
|
23
|
-
provenance: { source: 'base' as const },
|
|
24
|
-
}));
|
|
25
|
-
|
|
26
|
-
const relationships: ModelRelationship[] = [];
|
|
27
|
-
for (const [fieldName, fieldConfig] of Object.entries(config.fields)) {
|
|
28
|
-
if (!fieldConfig.relationship) continue;
|
|
29
|
-
|
|
30
|
-
relationships.push({
|
|
31
|
-
type: fieldConfig.relationship.type,
|
|
32
|
-
from: qualifiedName,
|
|
33
|
-
field: fieldName,
|
|
34
|
-
to: fieldConfig.relationship.model,
|
|
35
|
-
foreignKey: fieldConfig.relationship.foreignKey,
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const model: ResolvedModel = {
|
|
40
|
-
qualifiedName,
|
|
41
|
-
app,
|
|
42
|
-
module,
|
|
43
|
-
name: config.name,
|
|
44
|
-
label: config.label,
|
|
45
|
-
auditLog: false,
|
|
46
|
-
traits: [],
|
|
47
|
-
fields,
|
|
48
|
-
indexes: [],
|
|
49
|
-
source: config.source,
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
return { model, relationships };
|
|
53
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
export interface ExternalFieldMapping {
|
|
2
|
-
from?: string;
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
export interface ComputedFieldConfig {
|
|
6
|
-
depends: string[];
|
|
7
|
-
compute: (record: Record<string, unknown>) => unknown;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface ExternalRelationshipConfig {
|
|
11
|
-
type: 'link' | 'hasMany';
|
|
12
|
-
model: string;
|
|
13
|
-
from?: string;
|
|
14
|
-
foreignKey?: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface ExternalFieldConfig {
|
|
18
|
-
type: 'string' | 'int' | 'decimal' | 'boolean' | 'date' | 'datetime' | 'json';
|
|
19
|
-
label?: string;
|
|
20
|
-
required?: boolean;
|
|
21
|
-
from?: string;
|
|
22
|
-
computed?: ComputedFieldConfig;
|
|
23
|
-
relationship?: ExternalRelationshipConfig;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface ExternalModelConfig {
|
|
27
|
-
name: string;
|
|
28
|
-
source: string;
|
|
29
|
-
module?: string;
|
|
30
|
-
label?: string;
|
|
31
|
-
fields: Record<string, ExternalFieldConfig>;
|
|
32
|
-
}
|
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { FixtureRegistry } from '../registry.js';
|
|
3
|
-
import { loadFixtures } from '../loader.js';
|
|
4
|
-
import type { FixtureDefinition } from '../types.js';
|
|
5
|
-
|
|
6
|
-
describe('FixtureRegistry', () => {
|
|
7
|
-
it('registers and retrieves fixtures', () => {
|
|
8
|
-
const registry = new FixtureRegistry();
|
|
9
|
-
registry.register({
|
|
10
|
-
model: 'accounting.account',
|
|
11
|
-
key: 'account_code',
|
|
12
|
-
records: [{ account_code: '1000', name: 'Assets' }],
|
|
13
|
-
});
|
|
14
|
-
expect(registry.getForModel('accounting.account')).toHaveLength(1);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it('rejects fixture with empty model', () => {
|
|
18
|
-
const registry = new FixtureRegistry();
|
|
19
|
-
expect(() => registry.register({ model: '', key: 'code', records: [{ code: '1' }] })).toThrow(
|
|
20
|
-
'model must not be empty',
|
|
21
|
-
);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it('rejects fixture with no records', () => {
|
|
25
|
-
const registry = new FixtureRegistry();
|
|
26
|
-
expect(() => registry.register({ model: 'x.y', key: 'code', records: [] })).toThrow(
|
|
27
|
-
'at least one record',
|
|
28
|
-
);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('resolves dependency order', () => {
|
|
32
|
-
const registry = new FixtureRegistry();
|
|
33
|
-
registry.register({
|
|
34
|
-
model: 'accounting.tax',
|
|
35
|
-
key: 'name',
|
|
36
|
-
depends: ['accounting.account'],
|
|
37
|
-
records: [{ name: 'VAT' }],
|
|
38
|
-
});
|
|
39
|
-
registry.register({
|
|
40
|
-
model: 'accounting.account',
|
|
41
|
-
key: 'code',
|
|
42
|
-
records: [{ code: '1000' }],
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
const order = registry.getLoadOrder();
|
|
46
|
-
const models = order.map((f) => f.model);
|
|
47
|
-
expect(models.indexOf('accounting.account')).toBeLessThan(models.indexOf('accounting.tax'));
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('filters by variant with fallback to default', () => {
|
|
51
|
-
const registry = new FixtureRegistry();
|
|
52
|
-
registry.register({
|
|
53
|
-
model: 'accounting.account',
|
|
54
|
-
key: 'code',
|
|
55
|
-
records: [{ code: '1000', name: 'Default' }],
|
|
56
|
-
});
|
|
57
|
-
registry.register({
|
|
58
|
-
model: 'accounting.account',
|
|
59
|
-
key: 'code',
|
|
60
|
-
variant: 'ID',
|
|
61
|
-
records: [{ code: '1-1000', name: 'Indonesia' }],
|
|
62
|
-
});
|
|
63
|
-
registry.register({
|
|
64
|
-
model: 'accounting.tax',
|
|
65
|
-
key: 'name',
|
|
66
|
-
records: [{ name: 'VAT' }],
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
const filtered = registry.getForVariant('ID');
|
|
70
|
-
const models = filtered.map((f) => f.model);
|
|
71
|
-
expect(models).toContain('accounting.account');
|
|
72
|
-
expect(models).toContain('accounting.tax');
|
|
73
|
-
// Should use ID variant for account
|
|
74
|
-
const accountFixture = filtered.find((f) => f.model === 'accounting.account');
|
|
75
|
-
expect(accountFixture?.records[0].name).toBe('Indonesia');
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
describe('Fixture Loader', () => {
|
|
80
|
-
function createMockDb() {
|
|
81
|
-
const tables: Record<string, any[]> = {};
|
|
82
|
-
|
|
83
|
-
const db: any = {
|
|
84
|
-
selectFrom: (table: string) => {
|
|
85
|
-
const rows = tables[table] ?? [];
|
|
86
|
-
const filters: Array<{ col: string; val: any }> = [];
|
|
87
|
-
const builder: any = {
|
|
88
|
-
selectAll: () => builder,
|
|
89
|
-
select: () => builder,
|
|
90
|
-
where: (col: string, op: string, val: any) => {
|
|
91
|
-
filters.push({ col, val });
|
|
92
|
-
return builder;
|
|
93
|
-
},
|
|
94
|
-
execute: async () => {
|
|
95
|
-
return rows.filter((r) => filters.every((f) => r[f.col] === f.val));
|
|
96
|
-
},
|
|
97
|
-
};
|
|
98
|
-
return builder;
|
|
99
|
-
},
|
|
100
|
-
insertInto: (table: string) => ({
|
|
101
|
-
values: (data: any) => ({
|
|
102
|
-
execute: async () => {
|
|
103
|
-
if (!tables[table]) tables[table] = [];
|
|
104
|
-
tables[table].push({ id: `id-${tables[table].length + 1}`, ...data });
|
|
105
|
-
},
|
|
106
|
-
}),
|
|
107
|
-
}),
|
|
108
|
-
updateTable: (table: string) => ({
|
|
109
|
-
set: (data: any) => ({
|
|
110
|
-
where: (col: string, op: string, val: any) => ({
|
|
111
|
-
execute: async () => {
|
|
112
|
-
const rows = tables[table] ?? [];
|
|
113
|
-
const row = rows.find((r) => r[col] === val);
|
|
114
|
-
if (row) Object.assign(row, data);
|
|
115
|
-
},
|
|
116
|
-
}),
|
|
117
|
-
}),
|
|
118
|
-
}),
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
return { db, tables };
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
it('inserts new records', async () => {
|
|
125
|
-
const { db, tables } = createMockDb();
|
|
126
|
-
const defs: FixtureDefinition[] = [
|
|
127
|
-
{
|
|
128
|
-
model: 'accounting.account',
|
|
129
|
-
key: 'account_code',
|
|
130
|
-
records: [
|
|
131
|
-
{ account_code: '1000', name: 'Assets' },
|
|
132
|
-
{ account_code: '2000', name: 'Liabilities' },
|
|
133
|
-
],
|
|
134
|
-
},
|
|
135
|
-
];
|
|
136
|
-
|
|
137
|
-
const result = await loadFixtures(db, defs);
|
|
138
|
-
expect(result.inserted).toBe(2);
|
|
139
|
-
expect(result.skipped).toBe(0);
|
|
140
|
-
expect(tables['accounting_account']).toHaveLength(2);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it('skips user-modified records (no fixture hash)', async () => {
|
|
144
|
-
const { db, tables } = createMockDb();
|
|
145
|
-
tables['accounting_account'] = [
|
|
146
|
-
{ account_code: '1000', name: 'User Modified', _fixture_hash: undefined },
|
|
147
|
-
];
|
|
148
|
-
|
|
149
|
-
const defs: FixtureDefinition[] = [
|
|
150
|
-
{
|
|
151
|
-
model: 'accounting.account',
|
|
152
|
-
key: 'account_code',
|
|
153
|
-
records: [{ account_code: '1000', name: 'Assets' }],
|
|
154
|
-
},
|
|
155
|
-
];
|
|
156
|
-
|
|
157
|
-
const result = await loadFixtures(db, defs);
|
|
158
|
-
expect(result.skipped).toBe(1);
|
|
159
|
-
expect(tables['accounting_account'][0].name).toBe('User Modified');
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it('force mode overwrites existing records', async () => {
|
|
163
|
-
const { db, tables } = createMockDb();
|
|
164
|
-
tables['accounting_account'] = [{ account_code: '1000', name: 'Old', _fixture_hash: null }];
|
|
165
|
-
|
|
166
|
-
const defs: FixtureDefinition[] = [
|
|
167
|
-
{
|
|
168
|
-
model: 'accounting.account',
|
|
169
|
-
key: 'account_code',
|
|
170
|
-
records: [{ account_code: '1000', name: 'Updated' }],
|
|
171
|
-
},
|
|
172
|
-
];
|
|
173
|
-
|
|
174
|
-
const result = await loadFixtures(db, defs, { force: true });
|
|
175
|
-
expect(result.inserted).toBe(1);
|
|
176
|
-
expect(tables['accounting_account'][0].name).toBe('Updated');
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it('resolves ref fields to record IDs', async () => {
|
|
180
|
-
const { db, tables } = createMockDb();
|
|
181
|
-
tables['accounting_account'] = [
|
|
182
|
-
{ id: 'acct-uuid', account_code: '2100', name: 'Tax Payable', _fixture_hash: 'x' },
|
|
183
|
-
];
|
|
184
|
-
|
|
185
|
-
const defs: FixtureDefinition[] = [
|
|
186
|
-
{
|
|
187
|
-
model: 'accounting.tax',
|
|
188
|
-
key: 'name',
|
|
189
|
-
records: [
|
|
190
|
-
{
|
|
191
|
-
name: 'VAT 11%',
|
|
192
|
-
rate: 11,
|
|
193
|
-
account: { ref: 'accounting.account', key: '2100' },
|
|
194
|
-
},
|
|
195
|
-
],
|
|
196
|
-
},
|
|
197
|
-
];
|
|
198
|
-
|
|
199
|
-
const result = await loadFixtures(db, defs);
|
|
200
|
-
expect(result.inserted).toBe(1);
|
|
201
|
-
expect(tables['accounting_tax'][0].account).toBe('acct-uuid');
|
|
202
|
-
});
|
|
203
|
-
});
|
package/src/fixtures/index.ts
DELETED