@objectql/core 3.0.1 → 4.0.1
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 +17 -3
- package/README.md +31 -9
- package/dist/ai-agent.d.ts +4 -3
- package/dist/ai-agent.js +10 -3
- package/dist/ai-agent.js.map +1 -1
- package/dist/app.d.ts +29 -6
- package/dist/app.js +117 -58
- package/dist/app.js.map +1 -1
- package/dist/formula-engine.d.ts +7 -0
- package/dist/formula-engine.js +9 -2
- package/dist/formula-engine.js.map +1 -1
- package/dist/formula-plugin.d.ts +52 -0
- package/dist/formula-plugin.js +107 -0
- package/dist/formula-plugin.js.map +1 -0
- package/dist/index.d.ts +16 -3
- package/dist/index.js +14 -3
- package/dist/index.js.map +1 -1
- package/dist/plugin.d.ts +89 -0
- package/dist/plugin.js +136 -0
- package/dist/plugin.js.map +1 -0
- package/dist/query/filter-translator.d.ts +39 -0
- package/dist/query/filter-translator.js +135 -0
- package/dist/query/filter-translator.js.map +1 -0
- package/dist/query/index.d.ts +22 -0
- package/dist/query/index.js +39 -0
- package/dist/query/index.js.map +1 -0
- package/dist/query/query-analyzer.d.ts +188 -0
- package/dist/query/query-analyzer.js +349 -0
- package/dist/query/query-analyzer.js.map +1 -0
- package/dist/query/query-builder.d.ts +29 -0
- package/dist/query/query-builder.js +71 -0
- package/dist/query/query-builder.js.map +1 -0
- package/dist/query/query-service.d.ts +152 -0
- package/dist/query/query-service.js +268 -0
- package/dist/query/query-service.js.map +1 -0
- package/dist/repository.d.ts +23 -2
- package/dist/repository.js +81 -14
- package/dist/repository.js.map +1 -1
- package/dist/util.d.ts +7 -0
- package/dist/util.js +18 -3
- package/dist/util.js.map +1 -1
- package/dist/validator-plugin.d.ts +56 -0
- package/dist/validator-plugin.js +106 -0
- package/dist/validator-plugin.js.map +1 -0
- package/dist/validator.d.ts +7 -0
- package/dist/validator.js +10 -8
- package/dist/validator.js.map +1 -1
- package/jest.config.js +16 -0
- package/package.json +7 -5
- package/src/ai-agent.ts +8 -0
- package/src/app.ts +136 -72
- package/src/formula-engine.ts +8 -0
- package/src/formula-plugin.ts +141 -0
- package/src/index.ts +28 -3
- package/src/plugin.ts +224 -0
- package/src/query/filter-translator.ts +148 -0
- package/src/query/index.ts +24 -0
- package/src/query/query-analyzer.ts +537 -0
- package/src/query/query-builder.ts +81 -0
- package/src/query/query-service.ts +393 -0
- package/src/repository.ts +101 -18
- package/src/util.ts +19 -3
- package/src/validator-plugin.ts +140 -0
- package/src/validator.ts +12 -5
- package/test/__mocks__/@objectstack/runtime.ts +255 -0
- package/test/app.test.ts +23 -35
- package/test/filter-syntax.test.ts +233 -0
- package/test/formula-engine.test.ts +8 -0
- package/test/formula-integration.test.ts +8 -0
- package/test/formula-plugin.test.ts +197 -0
- package/test/introspection.test.ts +8 -0
- package/test/mock-driver.ts +8 -0
- package/test/plugin-integration.test.ts +213 -0
- package/test/repository-validation.test.ts +8 -0
- package/test/repository.test.ts +8 -0
- package/test/util.test.ts +9 -1
- package/test/utils.ts +8 -0
- package/test/validator-plugin.test.ts +126 -0
- package/test/validator.test.ts +8 -0
- package/tsconfig.json +8 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/action.d.ts +0 -7
- package/dist/action.js +0 -23
- package/dist/action.js.map +0 -1
- package/dist/hook.d.ts +0 -8
- package/dist/hook.js +0 -25
- package/dist/hook.js.map +0 -1
- package/dist/object.d.ts +0 -3
- package/dist/object.js +0 -28
- package/dist/object.js.map +0 -1
- package/src/action.ts +0 -40
- package/src/hook.ts +0 -42
- package/src/object.ts +0 -26
- package/test/action.test.ts +0 -276
- package/test/hook.test.ts +0 -343
- package/test/object.test.ts +0 -183
package/test/hook.test.ts
DELETED
|
@@ -1,343 +0,0 @@
|
|
|
1
|
-
import { ObjectQL } from '../src';
|
|
2
|
-
import { MockDriver } from './mock-driver';
|
|
3
|
-
|
|
4
|
-
describe('ObjectQL Hooks', () => {
|
|
5
|
-
let app: ObjectQL;
|
|
6
|
-
let driver: MockDriver;
|
|
7
|
-
|
|
8
|
-
beforeEach(async () => {
|
|
9
|
-
driver = new MockDriver();
|
|
10
|
-
app = new ObjectQL({
|
|
11
|
-
datasources: {
|
|
12
|
-
default: driver
|
|
13
|
-
},
|
|
14
|
-
objects: {
|
|
15
|
-
'post': {
|
|
16
|
-
name: 'post',
|
|
17
|
-
fields: {
|
|
18
|
-
title: { type: 'text' },
|
|
19
|
-
status: { type: 'text' },
|
|
20
|
-
views: { type: 'number' }
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
});
|
|
25
|
-
await app.init();
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
describe('Find Hooks', () => {
|
|
29
|
-
it('should trigger beforeFind and modify query', async () => {
|
|
30
|
-
const repo = app.createContext({}).object('post');
|
|
31
|
-
|
|
32
|
-
let hookTriggered = false;
|
|
33
|
-
app.on('beforeFind', 'post', async (ctx) => {
|
|
34
|
-
hookTriggered = true;
|
|
35
|
-
(ctx as any).query = { ...(ctx as any).query, filters: [['status', '=', 'published']] };
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
const spyFind = jest.spyOn(driver, 'find');
|
|
39
|
-
|
|
40
|
-
await repo.find({});
|
|
41
|
-
|
|
42
|
-
expect(hookTriggered).toBe(true);
|
|
43
|
-
expect(spyFind).toHaveBeenCalledWith('post', { filters: [['status', '=', 'published']] }, expect.any(Object));
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('should trigger afterFind and transform results', async () => {
|
|
47
|
-
const repo = app.createContext({}).object('post');
|
|
48
|
-
|
|
49
|
-
app.on('afterFind', 'post', async (ctx) => {
|
|
50
|
-
if (Array.isArray(ctx.result)) {
|
|
51
|
-
ctx.result = ctx.result.map(item => ({
|
|
52
|
-
...item,
|
|
53
|
-
transformed: true
|
|
54
|
-
}));
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
const results = await repo.find({});
|
|
59
|
-
|
|
60
|
-
expect(results).toBeDefined();
|
|
61
|
-
// Results should be transformed even if empty
|
|
62
|
-
expect(Array.isArray(results)).toBe(true);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('should provide user context in beforeFind', async () => {
|
|
66
|
-
const repo = app.createContext({ userId: 'user123' }).object('post');
|
|
67
|
-
|
|
68
|
-
let capturedUser: any;
|
|
69
|
-
app.on('beforeFind', 'post', async (ctx) => {
|
|
70
|
-
capturedUser = ctx.user;
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
await repo.find({});
|
|
74
|
-
|
|
75
|
-
expect(capturedUser).toBeDefined();
|
|
76
|
-
expect(capturedUser.id).toBe('user123');
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
describe('Count Hooks', () => {
|
|
81
|
-
it('should trigger beforeCount and modify query', async () => {
|
|
82
|
-
const repo = app.createContext({}).object('post');
|
|
83
|
-
|
|
84
|
-
let hookTriggered = false;
|
|
85
|
-
app.on('beforeCount', 'post', async (ctx) => {
|
|
86
|
-
hookTriggered = true;
|
|
87
|
-
ctx.query = { filters: [['status', '=', 'published']] };
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
await repo.count({});
|
|
91
|
-
|
|
92
|
-
expect(hookTriggered).toBe(true);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('should trigger afterCount and access result', async () => {
|
|
96
|
-
const repo = app.createContext({}).object('post');
|
|
97
|
-
|
|
98
|
-
let capturedResult: any;
|
|
99
|
-
app.on('afterCount', 'post', async (ctx) => {
|
|
100
|
-
capturedResult = ctx.result;
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
const count = await repo.count({});
|
|
104
|
-
|
|
105
|
-
expect(capturedResult).toBeDefined();
|
|
106
|
-
expect(typeof capturedResult).toBe('number');
|
|
107
|
-
expect(count).toBe(capturedResult);
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
describe('Create Hooks', () => {
|
|
112
|
-
it('should trigger beforeCreate and modify data', async () => {
|
|
113
|
-
const repo = app.createContext({ userId: 'u1' }).object('post');
|
|
114
|
-
|
|
115
|
-
app.on('beforeCreate', 'post', async (ctx) => {
|
|
116
|
-
if (ctx.data) {
|
|
117
|
-
ctx.data.status = ctx.data.status || 'draft';
|
|
118
|
-
ctx.data.views = 0;
|
|
119
|
-
}
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
const created = await repo.create({ title: 'New Post' });
|
|
123
|
-
|
|
124
|
-
expect(created.status).toBe('draft');
|
|
125
|
-
expect(created.views).toBe(0);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it('should trigger afterCreate and access result', async () => {
|
|
129
|
-
const repo = app.createContext({ userId: 'u1' }).object('post');
|
|
130
|
-
|
|
131
|
-
let capturedResult: any;
|
|
132
|
-
app.on('afterCreate', 'post', async (ctx) => {
|
|
133
|
-
capturedResult = ctx.result;
|
|
134
|
-
if (ctx.result) {
|
|
135
|
-
ctx.result.augmented = true;
|
|
136
|
-
}
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
const created = await repo.create({ title: 'New Post' });
|
|
140
|
-
|
|
141
|
-
expect(capturedResult).toBeDefined();
|
|
142
|
-
expect(created._id).toBeDefined();
|
|
143
|
-
expect(created.created_by).toBe('u1');
|
|
144
|
-
expect(created.augmented).toBe(true);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('should provide api access in beforeCreate', async () => {
|
|
148
|
-
const repo = app.createContext({}).object('post');
|
|
149
|
-
|
|
150
|
-
app.on('beforeCreate', 'post', async (ctx) => {
|
|
151
|
-
// Check for duplicate titles
|
|
152
|
-
const existing = await ctx.api.count('post', { filters: [['title', '=', ctx.data?.title]] });
|
|
153
|
-
if (existing > 0) {
|
|
154
|
-
throw new Error('Title already exists');
|
|
155
|
-
}
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
await repo.create({ title: 'Unique Title' });
|
|
159
|
-
|
|
160
|
-
// This should work fine on first create
|
|
161
|
-
expect(true).toBe(true);
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
describe('Update Hooks', () => {
|
|
166
|
-
it('should trigger beforeUpdate with previousData', async () => {
|
|
167
|
-
const repo = app.createContext({}).object('post');
|
|
168
|
-
|
|
169
|
-
const created = await repo.create({ title: 'Original', status: 'draft' });
|
|
170
|
-
|
|
171
|
-
let capturedPrevious: any;
|
|
172
|
-
app.on('beforeUpdate', 'post', async (ctx) => {
|
|
173
|
-
capturedPrevious = ctx.previousData;
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
await repo.update(created._id, { title: 'Updated' });
|
|
177
|
-
|
|
178
|
-
expect(capturedPrevious).toBeDefined();
|
|
179
|
-
expect(capturedPrevious.title).toBe('Original');
|
|
180
|
-
expect(capturedPrevious.status).toBe('draft');
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it('should use isModified helper correctly', async () => {
|
|
184
|
-
const repo = app.createContext({}).object('post');
|
|
185
|
-
|
|
186
|
-
const created = await repo.create({ title: 'Test', status: 'draft', views: 0 });
|
|
187
|
-
|
|
188
|
-
let titleModified = false;
|
|
189
|
-
let statusModified = false;
|
|
190
|
-
app.on('beforeUpdate', 'post', async (ctx) => {
|
|
191
|
-
if ('isModified' in ctx) {
|
|
192
|
-
titleModified = ctx.isModified('title' as any);
|
|
193
|
-
statusModified = ctx.isModified('status' as any);
|
|
194
|
-
}
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
await repo.update(created._id, { title: 'New Title' });
|
|
198
|
-
|
|
199
|
-
expect(titleModified).toBe(true);
|
|
200
|
-
expect(statusModified).toBe(false);
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
it('should trigger afterUpdate with result', async () => {
|
|
204
|
-
const repo = app.createContext({}).object('post');
|
|
205
|
-
|
|
206
|
-
const created = await repo.create({ title: 'Test', status: 'draft' });
|
|
207
|
-
|
|
208
|
-
let capturedResult: any;
|
|
209
|
-
app.on('afterUpdate', 'post', async (ctx) => {
|
|
210
|
-
capturedResult = ctx.result;
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
await repo.update(created._id, { status: 'published' });
|
|
214
|
-
|
|
215
|
-
expect(capturedResult).toBeDefined();
|
|
216
|
-
expect(capturedResult.status).toBe('published');
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
it('should validate state transitions in beforeUpdate', async () => {
|
|
220
|
-
const repo = app.createContext({}).object('post');
|
|
221
|
-
|
|
222
|
-
const created = await repo.create({ title: 'Test', status: 'published' });
|
|
223
|
-
|
|
224
|
-
app.on('beforeUpdate', 'post', async (ctx) => {
|
|
225
|
-
if ('isModified' in ctx && ctx.isModified('status' as any)) {
|
|
226
|
-
if (ctx.previousData?.status === 'published' && ctx.data?.status === 'draft') {
|
|
227
|
-
throw new Error('Cannot revert published post to draft');
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
await expect(repo.update(created._id, { status: 'draft' }))
|
|
233
|
-
.rejects
|
|
234
|
-
.toThrow('Cannot revert published post to draft');
|
|
235
|
-
});
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
describe('Delete Hooks', () => {
|
|
239
|
-
it('should trigger beforeDelete with id and previousData', async () => {
|
|
240
|
-
const repo = app.createContext({}).object('post');
|
|
241
|
-
|
|
242
|
-
const created = await repo.create({ title: 'To Delete', status: 'draft' });
|
|
243
|
-
|
|
244
|
-
let capturedId: any;
|
|
245
|
-
let capturedPrevious: any;
|
|
246
|
-
app.on('beforeDelete', 'post', async (ctx) => {
|
|
247
|
-
capturedId = ctx.id;
|
|
248
|
-
capturedPrevious = ctx.previousData;
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
await repo.delete(created._id);
|
|
252
|
-
|
|
253
|
-
expect(capturedId).toBe(created._id);
|
|
254
|
-
expect(capturedPrevious).toBeDefined();
|
|
255
|
-
expect(capturedPrevious.title).toBe('To Delete');
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
it('should trigger afterDelete with result', async () => {
|
|
259
|
-
const repo = app.createContext({}).object('post');
|
|
260
|
-
|
|
261
|
-
const created = await repo.create({ title: 'To Delete' });
|
|
262
|
-
|
|
263
|
-
let capturedResult: any;
|
|
264
|
-
app.on('afterDelete', 'post', async (ctx) => {
|
|
265
|
-
capturedResult = ctx.result;
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
await repo.delete(created._id);
|
|
269
|
-
|
|
270
|
-
expect(capturedResult).toBeDefined();
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
it('should check dependencies in beforeDelete', async () => {
|
|
274
|
-
const repo = app.createContext({}).object('post');
|
|
275
|
-
|
|
276
|
-
const created = await repo.create({ title: 'Protected Post', status: 'published' });
|
|
277
|
-
|
|
278
|
-
app.on('beforeDelete', 'post', async (ctx) => {
|
|
279
|
-
if (ctx.previousData?.status === 'published') {
|
|
280
|
-
throw new Error('Cannot delete published posts');
|
|
281
|
-
}
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
await expect(repo.delete(created._id))
|
|
285
|
-
.rejects
|
|
286
|
-
.toThrow('Cannot delete published posts');
|
|
287
|
-
});
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
describe('State Sharing', () => {
|
|
291
|
-
it('should share state between before and after hooks', async () => {
|
|
292
|
-
const repo = app.createContext({}).object('post');
|
|
293
|
-
|
|
294
|
-
app.on('beforeCreate', 'post', async (ctx) => {
|
|
295
|
-
ctx.state.timestamp = Date.now();
|
|
296
|
-
ctx.state.customData = 'test';
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
let capturedState: any;
|
|
300
|
-
app.on('afterCreate', 'post', async (ctx) => {
|
|
301
|
-
capturedState = ctx.state;
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
await repo.create({ title: 'Test' });
|
|
305
|
-
|
|
306
|
-
expect(capturedState).toBeDefined();
|
|
307
|
-
expect(capturedState.timestamp).toBeDefined();
|
|
308
|
-
expect(capturedState.customData).toBe('test');
|
|
309
|
-
});
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
describe('Error Handling', () => {
|
|
313
|
-
it('should prevent operation when beforeCreate throws error', async () => {
|
|
314
|
-
const repo = app.createContext({}).object('post');
|
|
315
|
-
|
|
316
|
-
app.on('beforeCreate', 'post', async (ctx) => {
|
|
317
|
-
if (!ctx.data?.title || ctx.data.title.length < 5) {
|
|
318
|
-
throw new Error('Title must be at least 5 characters');
|
|
319
|
-
}
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
await expect(repo.create({ title: 'Hi' }))
|
|
323
|
-
.rejects
|
|
324
|
-
.toThrow('Title must be at least 5 characters');
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
it('should prevent update when beforeUpdate throws error', async () => {
|
|
328
|
-
const repo = app.createContext({}).object('post');
|
|
329
|
-
|
|
330
|
-
const created = await repo.create({ title: 'Test Post', status: 'draft' });
|
|
331
|
-
|
|
332
|
-
app.on('beforeUpdate', 'post', async (ctx) => {
|
|
333
|
-
if (ctx.data?.status === 'archived') {
|
|
334
|
-
throw new Error('Archiving is not allowed');
|
|
335
|
-
}
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
await expect(repo.update(created._id, { status: 'archived' }))
|
|
339
|
-
.rejects
|
|
340
|
-
.toThrow('Archiving is not allowed');
|
|
341
|
-
});
|
|
342
|
-
});
|
|
343
|
-
});
|
package/test/object.test.ts
DELETED
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
import { registerObjectHelper, getConfigsHelper } from '../src/object';
|
|
2
|
-
import { ObjectConfig, MetadataRegistry } from '@objectql/types';
|
|
3
|
-
|
|
4
|
-
describe('Object Helper Functions', () => {
|
|
5
|
-
let metadata: MetadataRegistry;
|
|
6
|
-
|
|
7
|
-
beforeEach(() => {
|
|
8
|
-
metadata = new MetadataRegistry();
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
describe('registerObjectHelper', () => {
|
|
12
|
-
it('should register object with normalized fields', () => {
|
|
13
|
-
const object: ObjectConfig = {
|
|
14
|
-
name: 'todo',
|
|
15
|
-
fields: {
|
|
16
|
-
title: { type: 'text' },
|
|
17
|
-
completed: { type: 'boolean' }
|
|
18
|
-
}
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
registerObjectHelper(metadata, object);
|
|
22
|
-
|
|
23
|
-
const registered = metadata.get<ObjectConfig>('object', 'todo');
|
|
24
|
-
expect(registered).toBeDefined();
|
|
25
|
-
expect(registered?.name).toBe('todo');
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('should add name property to fields', () => {
|
|
29
|
-
const object: ObjectConfig = {
|
|
30
|
-
name: 'todo',
|
|
31
|
-
fields: {
|
|
32
|
-
title: { type: 'text' },
|
|
33
|
-
status: { type: 'select', options: ['active', 'done'] }
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
registerObjectHelper(metadata, object);
|
|
38
|
-
|
|
39
|
-
const registered = metadata.get<ObjectConfig>('object', 'todo');
|
|
40
|
-
expect(registered?.fields?.title.name).toBe('title');
|
|
41
|
-
expect(registered?.fields?.status.name).toBe('status');
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('should not override existing name property', () => {
|
|
45
|
-
const object: ObjectConfig = {
|
|
46
|
-
name: 'todo',
|
|
47
|
-
fields: {
|
|
48
|
-
title: { type: 'text', name: 'customTitle' }
|
|
49
|
-
}
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
registerObjectHelper(metadata, object);
|
|
53
|
-
|
|
54
|
-
const registered = metadata.get<ObjectConfig>('object', 'todo');
|
|
55
|
-
expect(registered?.fields?.title.name).toBe('customTitle');
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('should handle object without fields', () => {
|
|
59
|
-
const object: ObjectConfig = {
|
|
60
|
-
name: 'empty'
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
registerObjectHelper(metadata, object);
|
|
64
|
-
|
|
65
|
-
const registered = metadata.get<ObjectConfig>('object', 'empty');
|
|
66
|
-
expect(registered).toBeDefined();
|
|
67
|
-
expect(registered?.name).toBe('empty');
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('should register object with complex field configurations', () => {
|
|
71
|
-
const object: ObjectConfig = {
|
|
72
|
-
name: 'project',
|
|
73
|
-
fields: {
|
|
74
|
-
name: {
|
|
75
|
-
type: 'text',
|
|
76
|
-
required: true,
|
|
77
|
-
unique: true,
|
|
78
|
-
max_length: 100
|
|
79
|
-
},
|
|
80
|
-
owner: {
|
|
81
|
-
type: 'lookup',
|
|
82
|
-
reference_to: 'users'
|
|
83
|
-
},
|
|
84
|
-
tags: {
|
|
85
|
-
type: 'select',
|
|
86
|
-
multiple: true,
|
|
87
|
-
options: ['urgent', 'important']
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
registerObjectHelper(metadata, object);
|
|
93
|
-
|
|
94
|
-
const registered = metadata.get<ObjectConfig>('object', 'project');
|
|
95
|
-
expect(registered?.fields?.name.required).toBe(true);
|
|
96
|
-
expect(registered?.fields?.name.unique).toBe(true);
|
|
97
|
-
expect(registered?.fields?.owner.reference_to).toBe('users');
|
|
98
|
-
expect(registered?.fields?.tags.multiple).toBe(true);
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
describe('getConfigsHelper', () => {
|
|
103
|
-
it('should return empty object when no objects registered', () => {
|
|
104
|
-
const configs = getConfigsHelper(metadata);
|
|
105
|
-
expect(configs).toEqual({});
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('should return all registered objects', () => {
|
|
109
|
-
const todo: ObjectConfig = {
|
|
110
|
-
name: 'todo',
|
|
111
|
-
fields: { title: { type: 'text' } }
|
|
112
|
-
};
|
|
113
|
-
const project: ObjectConfig = {
|
|
114
|
-
name: 'project',
|
|
115
|
-
fields: { name: { type: 'text' } }
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
registerObjectHelper(metadata, todo);
|
|
119
|
-
registerObjectHelper(metadata, project);
|
|
120
|
-
|
|
121
|
-
const configs = getConfigsHelper(metadata);
|
|
122
|
-
expect(Object.keys(configs)).toHaveLength(2);
|
|
123
|
-
expect(configs.todo).toBeDefined();
|
|
124
|
-
expect(configs.project).toBeDefined();
|
|
125
|
-
expect(configs.todo.name).toBe('todo');
|
|
126
|
-
expect(configs.project.name).toBe('project');
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it('should return configs as key-value pairs by object name', () => {
|
|
130
|
-
const objects: ObjectConfig[] = [
|
|
131
|
-
{ name: 'users', fields: { name: { type: 'text' } } },
|
|
132
|
-
{ name: 'tasks', fields: { title: { type: 'text' } } },
|
|
133
|
-
{ name: 'projects', fields: { name: { type: 'text' } } }
|
|
134
|
-
];
|
|
135
|
-
|
|
136
|
-
objects.forEach(obj => registerObjectHelper(metadata, obj));
|
|
137
|
-
|
|
138
|
-
const configs = getConfigsHelper(metadata);
|
|
139
|
-
expect(configs.users.name).toBe('users');
|
|
140
|
-
expect(configs.tasks.name).toBe('tasks');
|
|
141
|
-
expect(configs.projects.name).toBe('projects');
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it('should reflect latest state after registration', () => {
|
|
145
|
-
let configs = getConfigsHelper(metadata);
|
|
146
|
-
expect(Object.keys(configs)).toHaveLength(0);
|
|
147
|
-
|
|
148
|
-
registerObjectHelper(metadata, {
|
|
149
|
-
name: 'todo',
|
|
150
|
-
fields: { title: { type: 'text' } }
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
configs = getConfigsHelper(metadata);
|
|
154
|
-
expect(Object.keys(configs)).toHaveLength(1);
|
|
155
|
-
|
|
156
|
-
registerObjectHelper(metadata, {
|
|
157
|
-
name: 'project',
|
|
158
|
-
fields: { name: { type: 'text' } }
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
configs = getConfigsHelper(metadata);
|
|
162
|
-
expect(Object.keys(configs)).toHaveLength(2);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it('should return configs after unregistration', () => {
|
|
166
|
-
registerObjectHelper(metadata, {
|
|
167
|
-
name: 'todo',
|
|
168
|
-
fields: { title: { type: 'text' } }
|
|
169
|
-
});
|
|
170
|
-
registerObjectHelper(metadata, {
|
|
171
|
-
name: 'project',
|
|
172
|
-
fields: { name: { type: 'text' } }
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
metadata.unregister('object', 'todo');
|
|
176
|
-
|
|
177
|
-
const configs = getConfigsHelper(metadata);
|
|
178
|
-
expect(Object.keys(configs)).toHaveLength(1);
|
|
179
|
-
expect(configs.todo).toBeUndefined();
|
|
180
|
-
expect(configs.project).toBeDefined();
|
|
181
|
-
});
|
|
182
|
-
});
|
|
183
|
-
});
|