@objectstack/objectql 3.1.0 → 3.2.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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @objectstack/objectql@3.1.0 build /home/runner/work/spec/spec/packages/objectql
2
+ > @objectstack/objectql@3.2.0 build /home/runner/work/spec/spec/packages/objectql
3
3
  > tsup --config ../../tsup.config.ts
4
4
 
5
5
  CLI Building entry: src/index.ts
@@ -10,13 +10,13 @@
10
10
  CLI Cleaning output folder
11
11
  ESM Build start
12
12
  CJS Build start
13
- CJS dist/index.js 95.52 KB
14
- CJS dist/index.js.map 193.28 KB
15
- CJS ⚡️ Build success in 187ms
16
13
  ESM dist/index.mjs 93.78 KB
17
14
  ESM dist/index.mjs.map 191.98 KB
18
- ESM ⚡️ Build success in 191ms
15
+ ESM ⚡️ Build success in 179ms
16
+ CJS dist/index.js 95.52 KB
17
+ CJS dist/index.js.map 193.28 KB
18
+ CJS ⚡️ Build success in 181ms
19
19
  DTS Build start
20
- DTS ⚡️ Build success in 23689ms
21
- DTS dist/index.d.mts 74.26 KB
22
- DTS dist/index.d.ts 74.26 KB
20
+ DTS ⚡️ Build success in 22038ms
21
+ DTS dist/index.d.mts 77.32 KB
22
+ DTS dist/index.d.ts 77.32 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # @objectstack/objectql
2
2
 
3
+ ## 3.2.0
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [5901c29]
8
+ - @objectstack/spec@3.2.0
9
+ - @objectstack/core@3.2.0
10
+ - @objectstack/types@3.2.0
11
+
12
+ ## 3.1.1
13
+
14
+ ### Patch Changes
15
+
16
+ - Updated dependencies [953d667]
17
+ - @objectstack/spec@3.1.1
18
+ - @objectstack/core@3.1.1
19
+ - @objectstack/types@3.1.1
20
+
3
21
  ## 3.1.0
4
22
 
5
23
  ### Patch Changes
package/dist/index.d.mts CHANGED
@@ -462,6 +462,65 @@ declare class SchemaRegistry {
462
462
  recordTypes?: string[] | undefined;
463
463
  sharingModel?: "full" | "private" | "read" | "read_write" | undefined;
464
464
  keyPrefix?: string | undefined;
465
+ actions?: {
466
+ name: string;
467
+ label: string | {
468
+ key: string;
469
+ defaultValue?: string | undefined;
470
+ params?: Record<string, string | number | boolean> | undefined;
471
+ };
472
+ type: "url" | "script" | "modal" | "flow" | "api";
473
+ refreshAfter: boolean;
474
+ objectName?: string | undefined;
475
+ icon?: string | undefined;
476
+ locations?: ("list_toolbar" | "list_item" | "record_header" | "record_more" | "record_related" | "global_nav")[] | undefined;
477
+ component?: "action:button" | "action:icon" | "action:menu" | "action:group" | undefined;
478
+ target?: string | undefined;
479
+ execute?: string | undefined;
480
+ params?: {
481
+ name: string;
482
+ label: string | {
483
+ key: string;
484
+ defaultValue?: string | undefined;
485
+ params?: Record<string, string | number | boolean> | undefined;
486
+ };
487
+ type: "number" | "boolean" | "date" | "lookup" | "file" | "url" | "json" | "text" | "textarea" | "email" | "phone" | "password" | "markdown" | "html" | "richtext" | "currency" | "percent" | "datetime" | "time" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "master_detail" | "tree" | "image" | "avatar" | "video" | "audio" | "formula" | "summary" | "autonumber" | "location" | "address" | "code" | "color" | "rating" | "slider" | "signature" | "qrcode" | "progress" | "tags" | "vector";
488
+ required: boolean;
489
+ options?: {
490
+ label: string | {
491
+ key: string;
492
+ defaultValue?: string | undefined;
493
+ params?: Record<string, string | number | boolean> | undefined;
494
+ };
495
+ value: string;
496
+ }[] | undefined;
497
+ }[] | undefined;
498
+ variant?: "link" | "primary" | "secondary" | "danger" | "ghost" | undefined;
499
+ confirmText?: string | {
500
+ key: string;
501
+ defaultValue?: string | undefined;
502
+ params?: Record<string, string | number | boolean> | undefined;
503
+ } | undefined;
504
+ successMessage?: string | {
505
+ key: string;
506
+ defaultValue?: string | undefined;
507
+ params?: Record<string, string | number | boolean> | undefined;
508
+ } | undefined;
509
+ visible?: string | undefined;
510
+ disabled?: string | boolean | undefined;
511
+ shortcut?: string | undefined;
512
+ bulkEnabled?: boolean | undefined;
513
+ timeout?: number | undefined;
514
+ aria?: {
515
+ ariaLabel?: string | {
516
+ key: string;
517
+ defaultValue?: string | undefined;
518
+ params?: Record<string, string | number | boolean> | undefined;
519
+ } | undefined;
520
+ ariaDescribedBy?: string | undefined;
521
+ role?: string | undefined;
522
+ } | undefined;
523
+ }[] | undefined;
465
524
  } | {
466
525
  name: string;
467
526
  label: string | {
@@ -845,7 +904,7 @@ declare class SchemaRegistry {
845
904
  objectstack: string;
846
905
  } | undefined;
847
906
  };
848
- status: "error" | "disabled" | "installed" | "installing" | "upgrading" | "uninstalling";
907
+ status: "disabled" | "error" | "installed" | "installing" | "upgrading" | "uninstalling";
849
908
  enabled: boolean;
850
909
  installedAt?: string | undefined;
851
910
  updatedAt?: string | undefined;
package/dist/index.d.ts CHANGED
@@ -462,6 +462,65 @@ declare class SchemaRegistry {
462
462
  recordTypes?: string[] | undefined;
463
463
  sharingModel?: "full" | "private" | "read" | "read_write" | undefined;
464
464
  keyPrefix?: string | undefined;
465
+ actions?: {
466
+ name: string;
467
+ label: string | {
468
+ key: string;
469
+ defaultValue?: string | undefined;
470
+ params?: Record<string, string | number | boolean> | undefined;
471
+ };
472
+ type: "url" | "script" | "modal" | "flow" | "api";
473
+ refreshAfter: boolean;
474
+ objectName?: string | undefined;
475
+ icon?: string | undefined;
476
+ locations?: ("list_toolbar" | "list_item" | "record_header" | "record_more" | "record_related" | "global_nav")[] | undefined;
477
+ component?: "action:button" | "action:icon" | "action:menu" | "action:group" | undefined;
478
+ target?: string | undefined;
479
+ execute?: string | undefined;
480
+ params?: {
481
+ name: string;
482
+ label: string | {
483
+ key: string;
484
+ defaultValue?: string | undefined;
485
+ params?: Record<string, string | number | boolean> | undefined;
486
+ };
487
+ type: "number" | "boolean" | "date" | "lookup" | "file" | "url" | "json" | "text" | "textarea" | "email" | "phone" | "password" | "markdown" | "html" | "richtext" | "currency" | "percent" | "datetime" | "time" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "master_detail" | "tree" | "image" | "avatar" | "video" | "audio" | "formula" | "summary" | "autonumber" | "location" | "address" | "code" | "color" | "rating" | "slider" | "signature" | "qrcode" | "progress" | "tags" | "vector";
488
+ required: boolean;
489
+ options?: {
490
+ label: string | {
491
+ key: string;
492
+ defaultValue?: string | undefined;
493
+ params?: Record<string, string | number | boolean> | undefined;
494
+ };
495
+ value: string;
496
+ }[] | undefined;
497
+ }[] | undefined;
498
+ variant?: "link" | "primary" | "secondary" | "danger" | "ghost" | undefined;
499
+ confirmText?: string | {
500
+ key: string;
501
+ defaultValue?: string | undefined;
502
+ params?: Record<string, string | number | boolean> | undefined;
503
+ } | undefined;
504
+ successMessage?: string | {
505
+ key: string;
506
+ defaultValue?: string | undefined;
507
+ params?: Record<string, string | number | boolean> | undefined;
508
+ } | undefined;
509
+ visible?: string | undefined;
510
+ disabled?: string | boolean | undefined;
511
+ shortcut?: string | undefined;
512
+ bulkEnabled?: boolean | undefined;
513
+ timeout?: number | undefined;
514
+ aria?: {
515
+ ariaLabel?: string | {
516
+ key: string;
517
+ defaultValue?: string | undefined;
518
+ params?: Record<string, string | number | boolean> | undefined;
519
+ } | undefined;
520
+ ariaDescribedBy?: string | undefined;
521
+ role?: string | undefined;
522
+ } | undefined;
523
+ }[] | undefined;
465
524
  } | {
466
525
  name: string;
467
526
  label: string | {
@@ -845,7 +904,7 @@ declare class SchemaRegistry {
845
904
  objectstack: string;
846
905
  } | undefined;
847
906
  };
848
- status: "error" | "disabled" | "installed" | "installing" | "upgrading" | "uninstalling";
907
+ status: "disabled" | "error" | "installed" | "installing" | "upgrading" | "uninstalling";
849
908
  enabled: boolean;
850
909
  installedAt?: string | undefined;
851
910
  updatedAt?: string | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@objectstack/objectql",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Isomorphic ObjectQL Engine for ObjectStack",
6
6
  "main": "dist/index.js",
@@ -13,9 +13,9 @@
13
13
  }
14
14
  },
15
15
  "dependencies": {
16
- "@objectstack/core": "3.1.0",
17
- "@objectstack/spec": "3.1.0",
18
- "@objectstack/types": "3.1.0"
16
+ "@objectstack/core": "3.2.0",
17
+ "@objectstack/spec": "3.2.0",
18
+ "@objectstack/types": "3.2.0"
19
19
  },
20
20
  "devDependencies": {
21
21
  "typescript": "^5.0.0",
@@ -0,0 +1,242 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+
3
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
4
+ import { ObjectStackProtocolImplementation } from './protocol.js';
5
+
6
+ /**
7
+ * Tests for the Protocol Implementation's data methods (findData, getData).
8
+ * Validates that expand/populate/select parameters are correctly normalized
9
+ * and forwarded to the underlying engine.
10
+ */
11
+ describe('ObjectStackProtocolImplementation - Data Operations', () => {
12
+ let protocol: ObjectStackProtocolImplementation;
13
+ let mockEngine: any;
14
+
15
+ beforeEach(() => {
16
+ mockEngine = {
17
+ find: vi.fn().mockResolvedValue([]),
18
+ findOne: vi.fn().mockResolvedValue(null),
19
+ };
20
+ protocol = new ObjectStackProtocolImplementation(mockEngine);
21
+ });
22
+
23
+ // ═══════════════════════════════════════════════════════════════
24
+ // findData — expand/populate normalization
25
+ // ═══════════════════════════════════════════════════════════════
26
+
27
+ describe('findData', () => {
28
+ it('should normalize expand string to populate array', async () => {
29
+ await protocol.findData({ object: 'order_item', query: { expand: 'order,product' } });
30
+
31
+ expect(mockEngine.find).toHaveBeenCalledWith(
32
+ 'order_item',
33
+ expect.objectContaining({
34
+ populate: ['order', 'product'],
35
+ }),
36
+ );
37
+ // expand should be deleted from options
38
+ const callArgs = mockEngine.find.mock.calls[0][1];
39
+ expect(callArgs.expand).toBeUndefined();
40
+ expect(callArgs.$expand).toBeUndefined();
41
+ });
42
+
43
+ it('should normalize $expand (OData) to populate array', async () => {
44
+ await protocol.findData({ object: 'task', query: { $expand: 'assignee,project' } });
45
+
46
+ expect(mockEngine.find).toHaveBeenCalledWith(
47
+ 'task',
48
+ expect.objectContaining({
49
+ populate: ['assignee', 'project'],
50
+ }),
51
+ );
52
+ });
53
+
54
+ it('should pass populate array as-is if already an array', async () => {
55
+ await protocol.findData({ object: 'task', query: { populate: ['assignee'] } });
56
+
57
+ expect(mockEngine.find).toHaveBeenCalledWith(
58
+ 'task',
59
+ expect.objectContaining({
60
+ populate: ['assignee'],
61
+ }),
62
+ );
63
+ });
64
+
65
+ it('should normalize populate string to array', async () => {
66
+ await protocol.findData({ object: 'task', query: { populate: 'assignee,project' } });
67
+
68
+ expect(mockEngine.find).toHaveBeenCalledWith(
69
+ 'task',
70
+ expect.objectContaining({
71
+ populate: ['assignee', 'project'],
72
+ }),
73
+ );
74
+ });
75
+
76
+ it('should prefer explicit populate over expand', async () => {
77
+ await protocol.findData({
78
+ object: 'task',
79
+ query: { populate: ['assignee'], expand: 'project' },
80
+ });
81
+
82
+ // populate takes precedence; expand is not converted
83
+ expect(mockEngine.find).toHaveBeenCalledWith(
84
+ 'task',
85
+ expect.objectContaining({
86
+ populate: ['assignee'],
87
+ }),
88
+ );
89
+ });
90
+
91
+ it('should normalize expand array to populate array', async () => {
92
+ await protocol.findData({ object: 'task', query: { expand: ['owner', 'team'] } });
93
+
94
+ expect(mockEngine.find).toHaveBeenCalledWith(
95
+ 'task',
96
+ expect.objectContaining({
97
+ populate: ['owner', 'team'],
98
+ }),
99
+ );
100
+ });
101
+
102
+ it('should normalize select string to array', async () => {
103
+ await protocol.findData({ object: 'task', query: { select: 'name,status,assignee' } });
104
+
105
+ expect(mockEngine.find).toHaveBeenCalledWith(
106
+ 'task',
107
+ expect.objectContaining({
108
+ select: ['name', 'status', 'assignee'],
109
+ }),
110
+ );
111
+ });
112
+
113
+ it('should pass numeric pagination params correctly', async () => {
114
+ await protocol.findData({ object: 'task', query: { top: '10', skip: '20' } });
115
+
116
+ expect(mockEngine.find).toHaveBeenCalledWith(
117
+ 'task',
118
+ expect.objectContaining({
119
+ top: 10,
120
+ skip: 20,
121
+ }),
122
+ );
123
+ });
124
+
125
+ it('should work with no query options', async () => {
126
+ await protocol.findData({ object: 'task' });
127
+
128
+ expect(mockEngine.find).toHaveBeenCalledWith('task', {});
129
+ });
130
+
131
+ it('should return records and standard response shape', async () => {
132
+ mockEngine.find.mockResolvedValue([{ _id: 't1', name: 'Task 1' }]);
133
+
134
+ const result = await protocol.findData({ object: 'task', query: {} });
135
+
136
+ expect(result).toEqual(
137
+ expect.objectContaining({
138
+ object: 'task',
139
+ records: [{ _id: 't1', name: 'Task 1' }],
140
+ total: 1,
141
+ }),
142
+ );
143
+ });
144
+ });
145
+
146
+ // ═══════════════════════════════════════════════════════════════
147
+ // getData — expand/select normalization
148
+ // ═══════════════════════════════════════════════════════════════
149
+
150
+ describe('getData', () => {
151
+ it('should convert expand string to populate array', async () => {
152
+ mockEngine.findOne.mockResolvedValue({ _id: 'oi_1', name: 'Item 1' });
153
+
154
+ await protocol.getData({ object: 'order_item', id: 'oi_1', expand: 'order,product' });
155
+
156
+ expect(mockEngine.findOne).toHaveBeenCalledWith(
157
+ 'order_item',
158
+ expect.objectContaining({
159
+ filter: { _id: 'oi_1' },
160
+ populate: ['order', 'product'],
161
+ }),
162
+ );
163
+ });
164
+
165
+ it('should convert expand array to populate array', async () => {
166
+ mockEngine.findOne.mockResolvedValue({ _id: 't1' });
167
+
168
+ await protocol.getData({ object: 'task', id: 't1', expand: ['assignee', 'project'] });
169
+
170
+ expect(mockEngine.findOne).toHaveBeenCalledWith(
171
+ 'task',
172
+ expect.objectContaining({
173
+ populate: ['assignee', 'project'],
174
+ }),
175
+ );
176
+ });
177
+
178
+ it('should convert select string to array', async () => {
179
+ mockEngine.findOne.mockResolvedValue({ _id: 't1', name: 'Test' });
180
+
181
+ await protocol.getData({ object: 'task', id: 't1', select: 'name,status' });
182
+
183
+ expect(mockEngine.findOne).toHaveBeenCalledWith(
184
+ 'task',
185
+ expect.objectContaining({
186
+ select: ['name', 'status'],
187
+ }),
188
+ );
189
+ });
190
+
191
+ it('should pass both expand and select together', async () => {
192
+ mockEngine.findOne.mockResolvedValue({ _id: 'oi_1' });
193
+
194
+ await protocol.getData({
195
+ object: 'order_item',
196
+ id: 'oi_1',
197
+ expand: 'order',
198
+ select: ['name', 'total'],
199
+ });
200
+
201
+ expect(mockEngine.findOne).toHaveBeenCalledWith(
202
+ 'order_item',
203
+ expect.objectContaining({
204
+ filter: { _id: 'oi_1' },
205
+ populate: ['order'],
206
+ select: ['name', 'total'],
207
+ }),
208
+ );
209
+ });
210
+
211
+ it('should work without expand or select', async () => {
212
+ mockEngine.findOne.mockResolvedValue({ _id: 't1' });
213
+
214
+ await protocol.getData({ object: 'task', id: 't1' });
215
+
216
+ expect(mockEngine.findOne).toHaveBeenCalledWith(
217
+ 'task',
218
+ { filter: { _id: 't1' } },
219
+ );
220
+ });
221
+
222
+ it('should return standard GetDataResponse shape', async () => {
223
+ mockEngine.findOne.mockResolvedValue({ _id: 'oi_1', name: 'Item 1' });
224
+
225
+ const result = await protocol.getData({ object: 'order_item', id: 'oi_1' });
226
+
227
+ expect(result).toEqual({
228
+ object: 'order_item',
229
+ id: 'oi_1',
230
+ record: { _id: 'oi_1', name: 'Item 1' },
231
+ });
232
+ });
233
+
234
+ it('should throw when record not found', async () => {
235
+ mockEngine.findOne.mockResolvedValue(null);
236
+
237
+ await expect(
238
+ protocol.getData({ object: 'task', id: 'missing_id' })
239
+ ).rejects.toThrow('not found');
240
+ });
241
+ });
242
+ });