@objectql/core 1.1.0 → 1.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 +15 -6
- package/dist/action.d.ts +7 -0
- package/dist/action.js +23 -0
- package/dist/action.js.map +1 -0
- package/dist/app.d.ts +28 -0
- package/dist/app.js +211 -0
- package/dist/app.js.map +1 -0
- package/dist/driver.d.ts +2 -17
- package/dist/driver.js +52 -0
- package/dist/driver.js.map +1 -1
- package/dist/hook.d.ts +8 -0
- package/dist/hook.js +25 -0
- package/dist/hook.js.map +1 -0
- package/dist/index.d.ts +8 -25
- package/dist/index.js +8 -141
- package/dist/index.js.map +1 -1
- package/dist/loader.d.ts +9 -4
- package/dist/loader.js +206 -9
- package/dist/loader.js.map +1 -1
- package/dist/object.d.ts +3 -0
- package/dist/object.js +28 -0
- package/dist/object.js.map +1 -0
- package/dist/plugin.d.ts +2 -0
- package/dist/plugin.js +56 -0
- package/dist/plugin.js.map +1 -0
- package/dist/remote.d.ts +8 -0
- package/dist/remote.js +43 -0
- package/dist/remote.js.map +1 -0
- package/dist/repository.d.ts +3 -5
- package/dist/repository.js +107 -112
- package/dist/repository.js.map +1 -1
- package/jest.config.js +3 -0
- package/package.json +11 -7
- package/src/action.ts +40 -0
- package/src/app.ts +257 -0
- package/src/driver.ts +51 -21
- package/src/hook.ts +42 -0
- package/src/index.ts +8 -158
- package/src/loader.ts +184 -9
- package/src/object.ts +26 -0
- package/src/plugin.ts +53 -0
- package/src/remote.ts +50 -0
- package/src/repository.ts +123 -127
- package/test/action.test.ts +58 -0
- package/test/fixtures/project.action.js +8 -0
- package/test/hook.test.ts +60 -0
- package/test/loader.test.ts +1 -8
- package/test/metadata.test.ts +1 -1
- package/test/mock-driver.ts +1 -1
- package/test/remote.test.ts +119 -0
- package/test/repository.test.ts +42 -49
- package/test/utils.ts +54 -0
- package/tsconfig.json +7 -3
- package/tsconfig.tsbuildinfo +1 -1
- package/README.md +0 -53
- package/dist/metadata.d.ts +0 -104
- package/dist/metadata.js +0 -3
- package/dist/metadata.js.map +0 -1
- package/dist/query.d.ts +0 -10
- package/dist/query.js +0 -3
- package/dist/query.js.map +0 -1
- package/dist/registry.d.ts +0 -4
- package/dist/registry.js +0 -8
- package/dist/registry.js.map +0 -1
- package/dist/types.d.ts +0 -83
- package/dist/types.js +0 -6
- package/dist/types.js.map +0 -1
- package/src/metadata.ts +0 -143
- package/src/query.ts +0 -11
- package/src/registry.ts +0 -6
- package/src/types.ts +0 -115
- package/test/fixtures/project.action.ts +0 -6
package/src/repository.ts
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import { ObjectQLContext, IObjectQL, HookContext,
|
|
2
|
-
import { ObjectConfig, FieldConfig } from './metadata';
|
|
3
|
-
import { Driver } from './driver';
|
|
4
|
-
import { UnifiedQuery, FilterCriterion } from './query';
|
|
1
|
+
import { ObjectQLContext, IObjectQL, ObjectConfig, Driver, UnifiedQuery, HookContext, ActionContext, HookAPI, RetrievalHookContext, MutationHookContext, UpdateHookContext } from '@objectql/types';
|
|
5
2
|
|
|
6
3
|
export class ObjectRepository {
|
|
7
4
|
constructor(
|
|
@@ -31,97 +28,52 @@ export class ObjectRepository {
|
|
|
31
28
|
return obj;
|
|
32
29
|
}
|
|
33
30
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const obj = this.getSchema();
|
|
43
|
-
if (!obj.listeners || !obj.listeners[hookName]) return;
|
|
44
|
-
|
|
45
|
-
const hookFn = obj.listeners[hookName] as HookFunction;
|
|
46
|
-
|
|
47
|
-
// Construct HookContext
|
|
48
|
-
const hookContext: HookContext = {
|
|
49
|
-
ctx: this.context,
|
|
50
|
-
entity: this.objectName,
|
|
51
|
-
op: op,
|
|
52
|
-
utils: {
|
|
53
|
-
restrict: (criterion: FilterCriterion) => {
|
|
54
|
-
if (op !== 'find' && op !== 'count') {
|
|
55
|
-
throw new Error('utils.restrict is only available in query operations');
|
|
56
|
-
}
|
|
57
|
-
const query = dataOrQuery as UnifiedQuery;
|
|
58
|
-
if (!query.filters) {
|
|
59
|
-
query.filters = [criterion];
|
|
60
|
-
} else {
|
|
61
|
-
// Enclose existing filters in implicit AND group by array structure logic or explicit 'and'
|
|
62
|
-
// Implementation depends on how driver parses.
|
|
63
|
-
// Safe approach: filters = [ [criterion], 'and', [existing] ] or similar.
|
|
64
|
-
// For simplicity assuming array of terms means AND:
|
|
65
|
-
query.filters.push(criterion);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
},
|
|
69
|
-
getPreviousDoc: async () => {
|
|
70
|
-
// For update/delete, we might need the ID to find the doc.
|
|
71
|
-
// If doc has ID, use it.
|
|
72
|
-
// This is simplistic; usually 'update' takes 'id', we need to capture it from arguments.
|
|
73
|
-
if (op === 'create') return undefined;
|
|
74
|
-
if (dataOrQuery._id || dataOrQuery.id) {
|
|
75
|
-
return this.findOne(dataOrQuery._id || dataOrQuery.id);
|
|
76
|
-
}
|
|
77
|
-
return undefined;
|
|
78
|
-
}
|
|
31
|
+
private getHookAPI(): HookAPI {
|
|
32
|
+
return {
|
|
33
|
+
find: (obj, q) => this.context.object(obj).find(q),
|
|
34
|
+
findOne: (obj, id) => this.context.object(obj).findOne(id),
|
|
35
|
+
count: (obj, q) => this.context.object(obj).count(q),
|
|
36
|
+
create: (obj, data) => this.context.object(obj).create(data),
|
|
37
|
+
update: (obj, id, data) => this.context.object(obj).update(id, data),
|
|
38
|
+
delete: (obj, id) => this.context.object(obj).delete(id)
|
|
79
39
|
};
|
|
80
|
-
|
|
81
|
-
if (op === 'find' || op === 'count' || op === 'aggregate') {
|
|
82
|
-
hookContext.query = dataOrQuery;
|
|
83
|
-
} else {
|
|
84
|
-
hookContext.doc = dataOrQuery;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Pass ID manually if needed or attach to doc?
|
|
88
|
-
// For strictness, getPreviousDoc needs the ID passed to the operation.
|
|
89
|
-
// We'll rely on "doc" having the data being processed.
|
|
90
|
-
|
|
91
|
-
await hookFn(hookContext);
|
|
92
40
|
}
|
|
93
41
|
|
|
94
42
|
async find(query: UnifiedQuery = {}): Promise<any[]> {
|
|
95
|
-
|
|
96
|
-
|
|
43
|
+
const hookCtx: RetrievalHookContext = {
|
|
44
|
+
...this.context,
|
|
45
|
+
objectName: this.objectName,
|
|
46
|
+
operation: 'find',
|
|
47
|
+
api: this.getHookAPI(),
|
|
48
|
+
state: {},
|
|
49
|
+
query
|
|
50
|
+
};
|
|
51
|
+
await this.app.triggerHook('beforeFind', this.objectName, hookCtx);
|
|
97
52
|
|
|
98
|
-
// TODO: Apply basic filters like spaceId
|
|
99
|
-
const results = await this.getDriver().find(this.objectName, query, this.getOptions());
|
|
53
|
+
// TODO: Apply basic filters like spaceId
|
|
54
|
+
const results = await this.getDriver().find(this.objectName, hookCtx.query || {}, this.getOptions());
|
|
100
55
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const hookFn = this.getSchema().listeners!.afterFind!;
|
|
106
|
-
// Executing per result or once? Spec says "HookContext" has "doc".
|
|
107
|
-
// If finding list, might not match signature.
|
|
108
|
-
// Implemented per-item for now (caution: performance).
|
|
109
|
-
/*
|
|
110
|
-
for (const item of results) {
|
|
111
|
-
await this.executeHookForDoc('afterFind', 'find', item);
|
|
112
|
-
}
|
|
113
|
-
*/
|
|
114
|
-
}
|
|
115
|
-
return results;
|
|
56
|
+
hookCtx.result = results;
|
|
57
|
+
await this.app.triggerHook('afterFind', this.objectName, hookCtx);
|
|
58
|
+
|
|
59
|
+
return hookCtx.result as any[];
|
|
116
60
|
}
|
|
117
61
|
|
|
118
62
|
async findOne(idOrQuery: string | number | UnifiedQuery): Promise<any> {
|
|
119
63
|
if (typeof idOrQuery === 'string' || typeof idOrQuery === 'number') {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
64
|
+
const hookCtx: RetrievalHookContext = {
|
|
65
|
+
...this.context,
|
|
66
|
+
objectName: this.objectName,
|
|
67
|
+
operation: 'find',
|
|
68
|
+
api: this.getHookAPI(), state: {}, query: { _id: idOrQuery }
|
|
69
|
+
};
|
|
70
|
+
await this.app.triggerHook('beforeFind', this.objectName, hookCtx);
|
|
71
|
+
|
|
72
|
+
const result = await this.getDriver().findOne(this.objectName, idOrQuery, hookCtx.query, this.getOptions());
|
|
73
|
+
|
|
74
|
+
hookCtx.result = result;
|
|
75
|
+
await this.app.triggerHook('afterFind', this.objectName, hookCtx);
|
|
76
|
+
return hookCtx.result;
|
|
125
77
|
} else {
|
|
126
78
|
const results = await this.find(idOrQuery);
|
|
127
79
|
return results[0] || null;
|
|
@@ -129,60 +81,95 @@ export class ObjectRepository {
|
|
|
129
81
|
}
|
|
130
82
|
|
|
131
83
|
async count(filters: any): Promise<number> {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
84
|
+
const hookCtx: RetrievalHookContext = {
|
|
85
|
+
...this.context,
|
|
86
|
+
objectName: this.objectName,
|
|
87
|
+
operation: 'count',
|
|
88
|
+
api: this.getHookAPI(),
|
|
89
|
+
state: {},
|
|
90
|
+
query: filters
|
|
91
|
+
};
|
|
92
|
+
await this.app.triggerHook('beforeCount', this.objectName, hookCtx);
|
|
137
93
|
|
|
138
|
-
|
|
139
|
-
const obj = this.getSchema();
|
|
140
|
-
if (this.context.userId) doc.created_by = this.context.userId;
|
|
141
|
-
if (this.context.spaceId) doc.space_id = this.context.spaceId;
|
|
94
|
+
const result = await this.getDriver().count(this.objectName, hookCtx.query, this.getOptions());
|
|
142
95
|
|
|
143
|
-
|
|
96
|
+
hookCtx.result = result;
|
|
97
|
+
await this.app.triggerHook('afterCount', this.objectName, hookCtx);
|
|
98
|
+
return hookCtx.result as number;
|
|
99
|
+
}
|
|
144
100
|
|
|
145
|
-
|
|
101
|
+
async create(doc: any): Promise<any> {
|
|
102
|
+
const hookCtx: MutationHookContext = {
|
|
103
|
+
...this.context,
|
|
104
|
+
objectName: this.objectName,
|
|
105
|
+
operation: 'create',
|
|
106
|
+
state: {},
|
|
107
|
+
api: this.getHookAPI(),
|
|
108
|
+
data: doc
|
|
109
|
+
};
|
|
110
|
+
await this.app.triggerHook('beforeCreate', this.objectName, hookCtx);
|
|
111
|
+
const finalDoc = hookCtx.data || doc;
|
|
146
112
|
|
|
147
|
-
|
|
148
|
-
|
|
113
|
+
const obj = this.getSchema();
|
|
114
|
+
if (this.context.userId) finalDoc.created_by = this.context.userId;
|
|
115
|
+
if (this.context.spaceId) finalDoc.space_id = this.context.spaceId;
|
|
116
|
+
|
|
117
|
+
const result = await this.getDriver().create(this.objectName, finalDoc, this.getOptions());
|
|
118
|
+
|
|
119
|
+
hookCtx.result = result;
|
|
120
|
+
await this.app.triggerHook('afterCreate', this.objectName, hookCtx);
|
|
121
|
+
return hookCtx.result;
|
|
149
122
|
}
|
|
150
123
|
|
|
151
124
|
async update(id: string | number, doc: any, options?: any): Promise<any> {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
125
|
+
const hookCtx: UpdateHookContext = {
|
|
126
|
+
...this.context,
|
|
127
|
+
objectName: this.objectName,
|
|
128
|
+
operation: 'update',
|
|
129
|
+
state: {},
|
|
130
|
+
api: this.getHookAPI(),
|
|
131
|
+
id,
|
|
132
|
+
data: doc,
|
|
133
|
+
isModified: (field) => hookCtx.data ? Object.prototype.hasOwnProperty.call(hookCtx.data, field) : false
|
|
134
|
+
};
|
|
135
|
+
await this.app.triggerHook('beforeUpdate', this.objectName, hookCtx);
|
|
159
136
|
|
|
160
|
-
const result = await this.getDriver().update(this.objectName, id,
|
|
137
|
+
const result = await this.getDriver().update(this.objectName, id, hookCtx.data, this.getOptions(options));
|
|
161
138
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
await this.executeHook('afterUpdate', 'update', docWithId);
|
|
166
|
-
return result;
|
|
139
|
+
hookCtx.result = result;
|
|
140
|
+
await this.app.triggerHook('afterUpdate', this.objectName, hookCtx);
|
|
141
|
+
return hookCtx.result;
|
|
167
142
|
}
|
|
168
143
|
|
|
169
144
|
async delete(id: string | number): Promise<any> {
|
|
170
|
-
const
|
|
171
|
-
|
|
145
|
+
const hookCtx: MutationHookContext = {
|
|
146
|
+
...this.context,
|
|
147
|
+
objectName: this.objectName,
|
|
148
|
+
operation: 'delete',
|
|
149
|
+
state: {},
|
|
150
|
+
api: this.getHookAPI(),
|
|
151
|
+
id
|
|
152
|
+
};
|
|
153
|
+
await this.app.triggerHook('beforeDelete', this.objectName, hookCtx);
|
|
172
154
|
|
|
173
155
|
const result = await this.getDriver().delete(this.objectName, id, this.getOptions());
|
|
174
156
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
157
|
+
hookCtx.result = result;
|
|
158
|
+
await this.app.triggerHook('afterDelete', this.objectName, hookCtx);
|
|
159
|
+
return hookCtx.result;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async aggregate(query: any): Promise<any> {
|
|
178
163
|
const driver = this.getDriver();
|
|
179
164
|
if (!driver.aggregate) throw new Error("Driver does not support aggregate");
|
|
165
|
+
|
|
180
166
|
return driver.aggregate(this.objectName, query, this.getOptions());
|
|
181
167
|
}
|
|
182
168
|
|
|
183
169
|
async distinct(field: string, filters?: any): Promise<any[]> {
|
|
184
170
|
const driver = this.getDriver();
|
|
185
171
|
if (!driver.distinct) throw new Error("Driver does not support distinct");
|
|
172
|
+
|
|
186
173
|
return driver.distinct(this.objectName, field, filters, this.getOptions());
|
|
187
174
|
}
|
|
188
175
|
|
|
@@ -193,8 +180,8 @@ export class ObjectRepository {
|
|
|
193
180
|
}
|
|
194
181
|
|
|
195
182
|
async createMany(data: any[]): Promise<any> {
|
|
196
|
-
// TODO: Triggers per record?
|
|
197
183
|
const driver = this.getDriver();
|
|
184
|
+
|
|
198
185
|
if (!driver.createMany) {
|
|
199
186
|
// Fallback
|
|
200
187
|
const results = [];
|
|
@@ -218,15 +205,24 @@ export class ObjectRepository {
|
|
|
218
205
|
return driver.deleteMany(this.objectName, filters, this.getOptions());
|
|
219
206
|
}
|
|
220
207
|
|
|
221
|
-
async
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
}
|
|
230
|
-
|
|
208
|
+
async execute(actionName: string, id: string | number | undefined, params: any): Promise<any> {
|
|
209
|
+
const api: HookAPI = {
|
|
210
|
+
find: (obj, q) => this.context.object(obj).find(q),
|
|
211
|
+
findOne: (obj, id) => this.context.object(obj).findOne(id),
|
|
212
|
+
count: (obj, q) => this.context.object(obj).count(q),
|
|
213
|
+
create: (obj, data) => this.context.object(obj).create(data),
|
|
214
|
+
update: (obj, id, data) => this.context.object(obj).update(id, data),
|
|
215
|
+
delete: (obj, id) => this.context.object(obj).delete(id)
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const ctx: ActionContext = {
|
|
219
|
+
...this.context,
|
|
220
|
+
objectName: this.objectName,
|
|
221
|
+
actionName,
|
|
222
|
+
id,
|
|
223
|
+
input: params,
|
|
224
|
+
api
|
|
225
|
+
};
|
|
226
|
+
return await this.app.executeAction(this.objectName, actionName, ctx);
|
|
231
227
|
}
|
|
232
228
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { ObjectQL } from '../src';
|
|
2
|
+
import { MockDriver } from './utils';
|
|
3
|
+
|
|
4
|
+
describe('ObjectQL Actions', () => {
|
|
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
|
+
'invoice': {
|
|
16
|
+
name: 'invoice',
|
|
17
|
+
fields: {
|
|
18
|
+
amount: { type: 'number' },
|
|
19
|
+
status: { type: 'text' }
|
|
20
|
+
},
|
|
21
|
+
actions: {
|
|
22
|
+
'pay': {
|
|
23
|
+
label: 'Pay Invoice',
|
|
24
|
+
params: {
|
|
25
|
+
method: { type: 'text' }
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
await app.init();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should register and execute an action', async () => {
|
|
36
|
+
const repo = app.createContext({}).object('invoice');
|
|
37
|
+
|
|
38
|
+
let actionCalled = false;
|
|
39
|
+
app.registerAction('invoice', 'pay', async (ctx) => {
|
|
40
|
+
actionCalled = true;
|
|
41
|
+
expect(ctx.objectName).toBe('invoice');
|
|
42
|
+
expect(ctx.actionName).toBe('pay');
|
|
43
|
+
expect(ctx.id).toBe('inv-123');
|
|
44
|
+
expect(ctx.input.method).toBe('credit_card');
|
|
45
|
+
return { success: true, paid: true };
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const result = await repo.execute('pay', 'inv-123', { method: 'credit_card' });
|
|
49
|
+
|
|
50
|
+
expect(actionCalled).toBe(true);
|
|
51
|
+
expect(result.success).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should throw error if action not registered', async () => {
|
|
55
|
+
const repo = app.createContext({}).object('invoice');
|
|
56
|
+
await expect(repo.execute('refund', '1', {})).rejects.toThrow("Action 'refund' not found for object 'invoice'");
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { ObjectQL } from '../src';
|
|
2
|
+
import { MockDriver } from './utils';
|
|
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
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
await app.init();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should trigger beforeFind and modify query', async () => {
|
|
28
|
+
const repo = app.createContext({}).object('post');
|
|
29
|
+
|
|
30
|
+
let hookTriggered = false;
|
|
31
|
+
app.on('beforeFind', 'post', async (ctx) => {
|
|
32
|
+
hookTriggered = true;
|
|
33
|
+
(ctx as any).query = { ...(ctx as any).query, filters: [['status', '=', 'published']] };
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Mock driver find to check query
|
|
37
|
+
const spyFind = jest.spyOn(driver, 'find');
|
|
38
|
+
|
|
39
|
+
await repo.find({});
|
|
40
|
+
|
|
41
|
+
expect(hookTriggered).toBe(true);
|
|
42
|
+
expect(spyFind).toHaveBeenCalledWith('post', { filters: [['status', '=', 'published']] }, expect.any(Object));
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should trigger afterCreate and return result', async () => {
|
|
46
|
+
const repo = app.createContext({ userId: 'u1' }).object('post');
|
|
47
|
+
|
|
48
|
+
app.on('afterCreate', 'post', async (ctx) => {
|
|
49
|
+
if (ctx.result) {
|
|
50
|
+
ctx.result.augmented = true;
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const created = await repo.create({ title: 'New Post' });
|
|
55
|
+
|
|
56
|
+
expect(created.id).toBeDefined();
|
|
57
|
+
expect(created.created_by).toBe('u1');
|
|
58
|
+
expect(created.augmented).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
});
|
package/test/loader.test.ts
CHANGED
|
@@ -11,12 +11,5 @@ describe('Loader', () => {
|
|
|
11
11
|
expect(configs['project'].fields).toBeDefined();
|
|
12
12
|
expect(configs['project'].fields.name).toBeDefined();
|
|
13
13
|
});
|
|
14
|
-
|
|
15
|
-
it('should load actions from .action.ts files', () => {
|
|
16
|
-
const fixturesDir = path.join(__dirname, 'fixtures');
|
|
17
|
-
const configs = loadObjectConfigs(fixturesDir);
|
|
18
|
-
expect(configs['project'].actions).toBeDefined();
|
|
19
|
-
expect(configs['project'].actions!.closeProject).toBeDefined();
|
|
20
|
-
expect(typeof configs['project'].actions!.closeProject.handler).toBe('function');
|
|
21
|
-
});
|
|
22
14
|
});
|
|
15
|
+
|
package/test/metadata.test.ts
CHANGED
package/test/mock-driver.ts
CHANGED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
|
|
2
|
+
import { ObjectQL } from '../src';
|
|
3
|
+
import { ObjectConfig } from '@objectql/types';
|
|
4
|
+
|
|
5
|
+
describe('ObjectQL Remote Federation', () => {
|
|
6
|
+
let originalFetch: any;
|
|
7
|
+
|
|
8
|
+
beforeAll(() => {
|
|
9
|
+
originalFetch = global.fetch;
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
afterAll(() => {
|
|
13
|
+
global.fetch = originalFetch;
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should load remote objects and proxy queries', async () => {
|
|
17
|
+
// 1. Mock Fetch
|
|
18
|
+
const mockFetch = jest.fn();
|
|
19
|
+
global.fetch = mockFetch;
|
|
20
|
+
|
|
21
|
+
const remoteUrl = 'http://remote-service:3000';
|
|
22
|
+
|
|
23
|
+
// Mock Responses
|
|
24
|
+
mockFetch.mockImplementation(async (url: string, options: any) => {
|
|
25
|
+
// A. Metadata List
|
|
26
|
+
if (url === `${remoteUrl}/api/metadata/objects`) {
|
|
27
|
+
return {
|
|
28
|
+
ok: true,
|
|
29
|
+
json: async () => ({
|
|
30
|
+
objects: [
|
|
31
|
+
{ name: 'remote_user', label: 'Remote User' }
|
|
32
|
+
]
|
|
33
|
+
})
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// B. Object Detail
|
|
38
|
+
if (url === `${remoteUrl}/api/metadata/objects/remote_user`) {
|
|
39
|
+
return {
|
|
40
|
+
ok: true,
|
|
41
|
+
json: async () => ({
|
|
42
|
+
name: 'remote_user',
|
|
43
|
+
fields: {
|
|
44
|
+
name: { type: 'text' },
|
|
45
|
+
email: { type: 'text' }
|
|
46
|
+
}
|
|
47
|
+
} as ObjectConfig)
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// C. Data Query (find)
|
|
52
|
+
if (url === `${remoteUrl}/api/objectql`) {
|
|
53
|
+
const body = JSON.parse(options.body);
|
|
54
|
+
if (body.op === 'find' && body.object === 'remote_user') {
|
|
55
|
+
return {
|
|
56
|
+
ok: true,
|
|
57
|
+
json: async () => ({
|
|
58
|
+
data: [
|
|
59
|
+
{ id: 1, name: 'Alice', email: 'alice@example.com' }
|
|
60
|
+
]
|
|
61
|
+
})
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { ok: false, status: 404 };
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// 2. Init ObjectQL with remotes
|
|
70
|
+
const app = new ObjectQL({
|
|
71
|
+
remotes: [remoteUrl]
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
await app.init();
|
|
75
|
+
|
|
76
|
+
// 3. Verify Schema is loaded
|
|
77
|
+
const config = app.getObject('remote_user');
|
|
78
|
+
expect(config).toBeDefined();
|
|
79
|
+
expect(config?.datasource).toBe(`remote:${remoteUrl}`);
|
|
80
|
+
|
|
81
|
+
// 4. Verify Query is proxied
|
|
82
|
+
// Note: 'object()' is on Context, not App. We need to create a context first.
|
|
83
|
+
const ctx = app.createContext({});
|
|
84
|
+
const users = await ctx.object('remote_user').find();
|
|
85
|
+
|
|
86
|
+
expect(users).toHaveLength(1);
|
|
87
|
+
expect(users[0].name).toBe('Alice');
|
|
88
|
+
|
|
89
|
+
// Verify fetch was called correctly
|
|
90
|
+
expect(mockFetch).toHaveBeenCalledTimes(3);
|
|
91
|
+
// 1. api/metadata/objects -> List
|
|
92
|
+
// 2. api/metadata/objects/remote_user -> Detail
|
|
93
|
+
// 3. api/objectql -> Query
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should handle remote errors gracefully', async () => {
|
|
97
|
+
const mockFetch = jest.fn();
|
|
98
|
+
global.fetch = mockFetch;
|
|
99
|
+
const remoteUrl = 'http://broken-service:3000';
|
|
100
|
+
|
|
101
|
+
// Mock Failure
|
|
102
|
+
mockFetch.mockResolvedValue({
|
|
103
|
+
ok: false,
|
|
104
|
+
status: 500,
|
|
105
|
+
statusText: 'Internal Server Error'
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const app = new ObjectQL({
|
|
109
|
+
remotes: [remoteUrl]
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Should not throw, just log warning (which we can spy on if we want, but preventing crash is key)
|
|
113
|
+
await expect(app.init()).resolves.not.toThrow();
|
|
114
|
+
|
|
115
|
+
// Object shouldn't exist
|
|
116
|
+
const config = app.getObject('remote_user');
|
|
117
|
+
expect(config).toBeUndefined();
|
|
118
|
+
});
|
|
119
|
+
});
|