-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathcreateKernel.ts
More file actions
217 lines (195 loc) · 7.84 KB
/
Copy pathcreateKernel.ts
File metadata and controls
217 lines (195 loc) · 7.84 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
/**
* Shared Kernel Factory for MSW Mock Environment
*
* Creates a fully bootstrapped ObjectStack kernel for use in both
* browser (MSW setupWorker) and test (MSW setupServer) environments.
*
* Uses MSWPlugin from @objectstack/plugin-msw to expose the full
* ObjectStack protocol via MSW. This ensures filter/sort/top/pagination
* work identically to server mode.
*
* Follows the same pattern as @objectstack/studio's createKernel —
* see https://github.com/objectstack-ai/spec
*/
import { ObjectKernel, DriverPlugin, AppPlugin } from '@objectstack/runtime';
import { ObjectQLPlugin } from '@objectstack/objectql';
import { InMemoryDriver } from '@objectstack/driver-memory';
import { MSWPlugin } from '@objectstack/plugin-msw';
import type { MSWPluginOptions } from '@objectstack/plugin-msw';
export interface KernelOptions {
/** Application configuration (defineStack output) */
appConfig: any;
/** Whether to skip system validation (useful in browser) */
skipSystemValidation?: boolean;
/** MSWPlugin options; when provided, MSWPlugin is added to the kernel. */
mswOptions?: MSWPluginOptions;
/**
* InMemoryDriver persistence configuration.
*
* - `'auto'` (default) — auto-detect environment (browser → localStorage, Node.js → file)
* - `'local'` — force localStorage persistence (browser only)
* - `false` — disable persistence entirely (useful in tests)
*
* When omitted, defaults to `'auto'`.
*/
persistence?: false | 'auto' | 'local' | 'file';
}
export interface KernelResult {
kernel: ObjectKernel;
driver: InMemoryDriver;
/** The MSWPlugin instance (if mswOptions was provided). */
mswPlugin?: MSWPlugin;
}
/**
* Install a lightweight broker shim on the kernel so that
* HttpDispatcher (used by MSWPlugin) can route data/metadata
* calls through the ObjectStack protocol service.
*
* A full Moleculer-based broker is only available in server mode
* (HonoServerPlugin). In MSW/browser mode we bridge the gap with
* this thin adapter that delegates to the protocol service.
*/
async function installBrokerShim(kernel: ObjectKernel): Promise<void> {
let protocol: any;
try {
protocol = await kernel.getService('protocol');
} catch {
return;
}
if (!protocol) return;
(kernel as any).broker = {
async call(action: string, params: any = {}) {
const [service, method] = action.split('.');
if (service === 'data') {
switch (method) {
case 'query':
return protocol.findData({ object: params.object, query: params.query ?? params });
case 'get':
return protocol.getData({ object: params.object, id: params.id });
case 'create':
return protocol.createData({ object: params.object, data: params.data });
case 'update':
return protocol.updateData({ object: params.object, id: params.id, data: params.data });
case 'delete':
return protocol.deleteData({ object: params.object, id: params.id });
case 'batch':
return protocol.batchData?.({ object: params.object, ...params }) ?? { results: [] };
}
}
if (service === 'metadata') {
if (method === 'types') return protocol.getMetaTypes({});
if (method === 'getObject') {
return protocol.getMetaItem({ type: 'object', name: params.objectName });
}
if (method === 'saveItem') {
return protocol.saveMetaItem?.({ type: params.type, name: params.name, item: params.item });
}
if (method.startsWith('get')) {
const type = method.replace('get', '').toLowerCase();
return protocol.getMetaItem({ type, name: params.name });
}
// list-style calls: metadata.objects, metadata.apps, etc.
return protocol.getMetaItems({ type: method, packageId: params.packageId });
}
throw new Error(`[BrokerShim] Unhandled action: ${action}`);
},
};
}
/**
* Sync `_id` with `id` on all records in the InMemoryDriver.
*
* The ObjectQL protocol uses `_id` for record identity lookups
* (e.g. `filter: { _id: id }`), but InMemoryDriver stores the
* generated identity as `id`. Seed data may also carry its own
* `_id` that differs from the driver-assigned `id`.
*
* When seed data provides an explicit `_id`, that value is promoted
* to `id` so that record identifiers remain stable across page
* refreshes (the driver would otherwise generate a new timestamp-based
* `id` every time the in-memory kernel reboots).
*
* When no explicit `_id` exists, `_id` is derived from the
* driver-assigned `id` so protocol lookups still work.
*/
function syncDriverIds(driver: InMemoryDriver): void {
const db = (driver as any).db as Record<string, any[]>;
for (const records of Object.values(db)) {
for (const record of records) {
if (record._id != null && record._id !== record.id) {
// Seed data carries an explicit _id → promote it to canonical id
record.id = record._id;
} else if (record.id) {
// No explicit seed _id → derive _id from driver-assigned id
record._id = record.id;
}
}
}
}
/**
* Wrap the driver's `create` method so that every newly created
* record also gets `_id` set to the same value as `id`.
*/
function patchDriverCreate(driver: InMemoryDriver): void {
const originalCreate = driver.create.bind(driver);
(driver as any).create = async (object: string, data: any, options?: any) => {
const result = await originalCreate(object, data, options);
// Patch the stored record in-place
const table = (driver as any).db[object] as any[] | undefined;
if (table) {
const stored = table[table.length - 1];
if (stored && stored.id === result.id) {
stored._id = stored.id;
}
}
// Also patch the returned copy
if (!(result as any)._id) (result as any)._id = result.id;
return result;
};
}
/**
* Create and bootstrap an ObjectStack kernel with in-memory driver.
*
* This is the single factory used by both browser.ts and server.ts
* so that kernel setup logic is not duplicated.
*/
export async function createKernel(options: KernelOptions): Promise<KernelResult> {
const { appConfig, skipSystemValidation = true, mswOptions, persistence } = options;
const driver = new InMemoryDriver(
persistence !== undefined ? { persistence } : undefined,
);
const kernel = new ObjectKernel({
skipSystemValidation
});
await kernel.use(new ObjectQLPlugin());
await kernel.use(new DriverPlugin(driver, 'memory'));
await kernel.use(new AppPlugin(appConfig));
let mswPlugin: MSWPlugin | undefined;
if (mswOptions) {
// Install a protocol-based broker shim BEFORE MSWPlugin's start phase
// so that HttpDispatcher (inside MSWPlugin) can resolve data/metadata
// calls without requiring a full Moleculer broker.
await installBrokerShim(kernel);
mswPlugin = new MSWPlugin(mswOptions);
await kernel.use(mswPlugin);
}
await kernel.bootstrap();
// After bootstrap, sync _id with id for all seed-data records and
// wrap create() so that new records also get _id = id.
syncDriverIds(driver);
patchDriverCreate(driver);
// Re-install broker shim after bootstrap to ensure the protocol service
// is fully initialised (some plugins register services during start phase).
if (mswOptions) {
await installBrokerShim(kernel);
}
// Initialise persistence adapter and load any previously persisted data.
// On first load this is a no-op (empty localStorage); on subsequent page
// refreshes the persisted data overwrites the seed data so that user
// changes survive a browser reload.
await driver.connect();
// Ensure the current database state (seed data on first load, or the
// just-restored persisted snapshot) is flushed to the persistence layer
// so that localStorage always contains the latest data.
await driver.flush();
return { kernel, driver, mswPlugin };
}