@objectstack/runtime 3.3.0 → 4.0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@objectstack/runtime",
3
- "version": "3.3.0",
3
+ "version": "4.0.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "ObjectStack Core Runtime & Query Engine",
6
6
  "type": "module",
@@ -15,14 +15,14 @@
15
15
  },
16
16
  "dependencies": {
17
17
  "zod": "^4.3.6",
18
- "@objectstack/core": "3.3.0",
19
- "@objectstack/rest": "3.3.0",
20
- "@objectstack/spec": "3.3.0",
21
- "@objectstack/types": "3.3.0"
18
+ "@objectstack/core": "4.0.0",
19
+ "@objectstack/rest": "4.0.0",
20
+ "@objectstack/spec": "4.0.0",
21
+ "@objectstack/types": "4.0.0"
22
22
  },
23
23
  "devDependencies": {
24
- "typescript": "^5.0.0",
25
- "vitest": "^4.1.0"
24
+ "typescript": "^6.0.2",
25
+ "vitest": "^4.1.2"
26
26
  },
27
27
  "scripts": {
28
28
  "build": "tsup --config ../../tsup.config.ts",
@@ -46,6 +46,21 @@ describe('HttpDispatcher Root Handling', () => {
46
46
  expect(data.routes.metadata).toBe('/meta');
47
47
  });
48
48
 
49
+ it('should handle GET /discovery (protocol-standard route)', async () => {
50
+ const context = { request: {} };
51
+ const result = await dispatcher.dispatch('GET', '/discovery', undefined, {}, context);
52
+
53
+ expect(result.handled).toBe(true);
54
+ expect(result.response).toBeDefined();
55
+ expect(result.response?.status).toBe(200);
56
+
57
+ const data = result.response?.body?.data;
58
+ expect(data).toBeDefined();
59
+ expect(data.name).toBe('ObjectOS');
60
+ expect(data.version).toBe('1.0.0');
61
+ expect(data.routes).toBeDefined();
62
+ });
63
+
49
64
  it('should NOT handle POST request to root path ("")', async () => {
50
65
  const context = { request: {} };
51
66
  const method = 'POST';
@@ -645,9 +645,10 @@ describe('HttpDispatcher', () => {
645
645
  );
646
646
 
647
647
  expect(result.handled).toBe(true);
648
+ // top → limit and skip → offset are normalized by the dispatcher
648
649
  expect(mockBroker.call).toHaveBeenCalledWith(
649
650
  'data.query',
650
- { object: 'task', query },
651
+ { object: 'task', query: { populate: 'assignee,project', limit: '10', offset: '0' } },
651
652
  { request: {} }
652
653
  );
653
654
  });
@@ -585,8 +585,49 @@ export class HttpDispatcher {
585
585
  } else {
586
586
  // GET /data/:object (List)
587
587
  if (m === 'GET') {
588
+ // ── Normalize HTTP transport params → Spec canonical (QueryAST) ──
589
+ // HTTP GET query params use transport-level names (filter, sort, top,
590
+ // skip, select, expand) which are normalized here to canonical
591
+ // QueryAST field names (where, orderBy, limit, offset, fields,
592
+ // expand) before forwarding to the broker layer.
593
+ // The protocol.ts findData() method performs a deeper normalization
594
+ // pass, but pre-normalizing here ensures the broker always receives
595
+ // Spec-canonical keys.
596
+ const normalized: Record<string, unknown> = { ...query };
597
+
598
+ // filter/filters → where
599
+ // Note: `filter` is the canonical HTTP *transport* parameter name
600
+ // (see HttpFindQueryParamsSchema). It is normalized here to the
601
+ // canonical *QueryAST* field name `where` before broker dispatch.
602
+ // `filters` (plural) is a deprecated alias for `filter`.
603
+ if (normalized.filter != null || normalized.filters != null) {
604
+ normalized.where = normalized.where ?? normalized.filter ?? normalized.filters;
605
+ delete normalized.filter;
606
+ delete normalized.filters;
607
+ }
608
+ // select → fields
609
+ if (normalized.select != null && normalized.fields == null) {
610
+ normalized.fields = normalized.select;
611
+ delete normalized.select;
612
+ }
613
+ // sort → orderBy
614
+ if (normalized.sort != null && normalized.orderBy == null) {
615
+ normalized.orderBy = normalized.sort;
616
+ delete normalized.sort;
617
+ }
618
+ // top → limit
619
+ if (normalized.top != null && normalized.limit == null) {
620
+ normalized.limit = normalized.top;
621
+ delete normalized.top;
622
+ }
623
+ // skip → offset
624
+ if (normalized.skip != null && normalized.offset == null) {
625
+ normalized.offset = normalized.skip;
626
+ delete normalized.skip;
627
+ }
628
+
588
629
  // Spec: broker returns FindDataResponse = { object, records, total?, hasMore? }
589
- const result = await broker.call('data.query', { object: objectName, query }, { request: context.request });
630
+ const result = await broker.call('data.query', { object: objectName, query: normalized }, { request: context.request });
590
631
  return { handled: true, response: this.success(result) };
591
632
  }
592
633
 
@@ -1154,9 +1195,10 @@ export class HttpDispatcher {
1154
1195
  async dispatch(method: string, path: string, body: any, query: any, context: HttpProtocolContext): Promise<HttpDispatcherResult> {
1155
1196
  const cleanPath = path.replace(/\/$/, ''); // Remove trailing slash if present, but strict on clean paths
1156
1197
 
1157
- // 0. Root Discovery Endpoint (GET /)
1158
- // Handles request to base URL (e.g. /api/v1) which MSW strips to empty string
1159
- if (cleanPath === '' && method === 'GET') {
1198
+ // 0. Discovery Endpoint (GET /discovery or GET /)
1199
+ // Standard route: /discovery (protocol-compliant)
1200
+ // Legacy route: / (empty path, for backward compatibility MSW strips base URL)
1201
+ if ((cleanPath === '/discovery' || cleanPath === '') && method === 'GET') {
1160
1202
  // We use '' as prefix since we are internal dispatcher
1161
1203
  const info = await this.getDiscoveryInfo('');
1162
1204
  return {
@@ -324,8 +324,8 @@ export class SeedLoaderService implements ISeedLoaderService {
324
324
  ): Promise<string | null> {
325
325
  try {
326
326
  const records = await this.engine.find(targetObject, {
327
- filter: { [targetField]: value },
328
- select: ['id'],
327
+ where: { [targetField]: value },
328
+ fields: ['id'],
329
329
  limit: 1,
330
330
  });
331
331
  if (records && records.length > 0) {
@@ -612,7 +612,7 @@ export class SeedLoaderService implements ISeedLoaderService {
612
612
  const map = new Map<string, any>();
613
613
  try {
614
614
  const records = await this.engine.find(objectName, {
615
- select: ['id', externalId],
615
+ fields: ['id', externalId],
616
616
  });
617
617
  for (const record of records || []) {
618
618
  const key = String(record[externalId] ?? '');
package/tsconfig.json CHANGED
@@ -2,7 +2,8 @@
2
2
  "extends": "../../tsconfig.json",
3
3
  "compilerOptions": {
4
4
  "outDir": "./dist",
5
- "rootDir": "./src"
5
+ "rootDir": "./src",
6
+ "types": ["node"]
6
7
  },
7
8
  "include": ["src/**/*"],
8
9
  "exclude": ["node_modules", "dist", "**/*.spec.ts", "**/*.test.ts"]