fauxbase 0.1.2 → 0.5.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/dist/index.cjs +1140 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +283 -8
- package/dist/index.d.ts +283 -8
- package/dist/index.js +1126 -13
- package/dist/index.js.map +1 -1
- package/package.json +8 -5
package/dist/index.js
CHANGED
|
@@ -36,6 +36,29 @@ var ForbiddenError = class extends FauxbaseError {
|
|
|
36
36
|
this.name = "ForbiddenError";
|
|
37
37
|
}
|
|
38
38
|
};
|
|
39
|
+
var NetworkError = class extends FauxbaseError {
|
|
40
|
+
constructor(message = "Network request failed") {
|
|
41
|
+
super(message, "NETWORK");
|
|
42
|
+
this.name = "NetworkError";
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
var TimeoutError = class extends FauxbaseError {
|
|
46
|
+
constructor(message = "Request timed out") {
|
|
47
|
+
super(message, "TIMEOUT");
|
|
48
|
+
this.name = "TimeoutError";
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
var HttpError = class extends FauxbaseError {
|
|
52
|
+
status;
|
|
53
|
+
constructor(message, status, details) {
|
|
54
|
+
super(message, "HTTP", details);
|
|
55
|
+
this.name = "HttpError";
|
|
56
|
+
this.status = status;
|
|
57
|
+
}
|
|
58
|
+
toJSON() {
|
|
59
|
+
return { ...super.toJSON(), status: this.status };
|
|
60
|
+
}
|
|
61
|
+
};
|
|
39
62
|
|
|
40
63
|
// src/registry.ts
|
|
41
64
|
var fieldRegistry = /* @__PURE__ */ new Map();
|
|
@@ -157,11 +180,18 @@ function afterUpdate() {
|
|
|
157
180
|
var Service = class {
|
|
158
181
|
driver;
|
|
159
182
|
resourceName;
|
|
183
|
+
client;
|
|
184
|
+
/** @internal */
|
|
185
|
+
_eventBus;
|
|
160
186
|
/** @internal — called by createClient to wire the service */
|
|
161
187
|
_init(driver, resourceName) {
|
|
162
188
|
this.driver = driver;
|
|
163
189
|
this.resourceName = resourceName;
|
|
164
190
|
}
|
|
191
|
+
/** @internal — called by createClient to give services access to the client */
|
|
192
|
+
_setClient(client) {
|
|
193
|
+
this.client = client;
|
|
194
|
+
}
|
|
165
195
|
async list(query = {}) {
|
|
166
196
|
return this.driver.list(this.resourceName, query);
|
|
167
197
|
}
|
|
@@ -173,27 +203,56 @@ var Service = class {
|
|
|
173
203
|
await this.runHooks("beforeCreate", data, allItems);
|
|
174
204
|
const result = await this.driver.create(this.resourceName, data);
|
|
175
205
|
await this.runHooks("afterCreate", result.data);
|
|
206
|
+
this.emitEvent("created", { data: result.data, id: result.data.id });
|
|
176
207
|
return result;
|
|
177
208
|
}
|
|
178
209
|
async update(id, data) {
|
|
179
210
|
await this.runHooks("beforeUpdate", id, data);
|
|
180
211
|
const result = await this.driver.update(this.resourceName, id, data);
|
|
181
212
|
await this.runHooks("afterUpdate", result.data);
|
|
213
|
+
this.emitEvent("updated", { data: result.data, id });
|
|
182
214
|
return result;
|
|
183
215
|
}
|
|
184
216
|
async delete(id) {
|
|
185
|
-
|
|
217
|
+
const result = await this.driver.delete(this.resourceName, id);
|
|
218
|
+
this.emitEvent("deleted", { data: result.data, id });
|
|
219
|
+
return result;
|
|
186
220
|
}
|
|
187
221
|
async count(filter) {
|
|
188
222
|
return this.driver.count(this.resourceName, filter);
|
|
189
223
|
}
|
|
190
224
|
get bulk() {
|
|
225
|
+
const self = this;
|
|
191
226
|
return {
|
|
192
|
-
create
|
|
193
|
-
|
|
194
|
-
|
|
227
|
+
async create(items) {
|
|
228
|
+
const result = await self.driver.bulkCreate(self.resourceName, items);
|
|
229
|
+
self.emitEvent("bulkCreated", { data: result.data });
|
|
230
|
+
return result;
|
|
231
|
+
},
|
|
232
|
+
async update(updates) {
|
|
233
|
+
const result = await self.driver.bulkUpdate(self.resourceName, updates);
|
|
234
|
+
self.emitEvent("bulkUpdated", { data: result.data, ids: updates.map((u) => u.id) });
|
|
235
|
+
return result;
|
|
236
|
+
},
|
|
237
|
+
async delete(ids) {
|
|
238
|
+
const result = await self.driver.bulkDelete(self.resourceName, ids);
|
|
239
|
+
self.emitEvent("bulkDeleted", { ids });
|
|
240
|
+
return result;
|
|
241
|
+
}
|
|
195
242
|
};
|
|
196
243
|
}
|
|
244
|
+
emitEvent(action, extra) {
|
|
245
|
+
if (!this._eventBus) return;
|
|
246
|
+
this._eventBus.emit({
|
|
247
|
+
action,
|
|
248
|
+
resource: this.resourceName,
|
|
249
|
+
data: extra.data,
|
|
250
|
+
id: extra.id,
|
|
251
|
+
ids: extra.ids,
|
|
252
|
+
timestamp: Date.now(),
|
|
253
|
+
source: "local"
|
|
254
|
+
});
|
|
255
|
+
}
|
|
197
256
|
async runHooks(hookType, ...args) {
|
|
198
257
|
const methods = getHooks(this.constructor, hookType);
|
|
199
258
|
for (const methodName of methods) {
|
|
@@ -202,6 +261,173 @@ var Service = class {
|
|
|
202
261
|
}
|
|
203
262
|
};
|
|
204
263
|
|
|
264
|
+
// src/auth.ts
|
|
265
|
+
var AuthService = class extends Service {
|
|
266
|
+
authState = null;
|
|
267
|
+
saveState = null;
|
|
268
|
+
httpDriver = null;
|
|
269
|
+
/** @internal — called by createClient to wire persistence */
|
|
270
|
+
_initAuth(loadState, saveState) {
|
|
271
|
+
this.saveState = saveState;
|
|
272
|
+
this.authState = loadState();
|
|
273
|
+
}
|
|
274
|
+
/** @internal — called by createClient when using HttpDriver */
|
|
275
|
+
_setHttpMode(driver) {
|
|
276
|
+
this.httpDriver = driver;
|
|
277
|
+
}
|
|
278
|
+
async login(credentials) {
|
|
279
|
+
if (this.httpDriver) {
|
|
280
|
+
return this.httpLogin(credentials);
|
|
281
|
+
}
|
|
282
|
+
return this.localLogin(credentials);
|
|
283
|
+
}
|
|
284
|
+
async register(data) {
|
|
285
|
+
if (this.httpDriver) {
|
|
286
|
+
return this.httpRegister(data);
|
|
287
|
+
}
|
|
288
|
+
return this.localRegister(data);
|
|
289
|
+
}
|
|
290
|
+
logout() {
|
|
291
|
+
this.authState = null;
|
|
292
|
+
this.persistState();
|
|
293
|
+
}
|
|
294
|
+
get currentUser() {
|
|
295
|
+
return this.authState ? { id: this.authState.userId, email: this.authState.email } : null;
|
|
296
|
+
}
|
|
297
|
+
get isLoggedIn() {
|
|
298
|
+
return this.authState !== null;
|
|
299
|
+
}
|
|
300
|
+
get token() {
|
|
301
|
+
return this.authState?.token ?? null;
|
|
302
|
+
}
|
|
303
|
+
hasRole(role) {
|
|
304
|
+
return this.authState?.role === role;
|
|
305
|
+
}
|
|
306
|
+
getAuthContext() {
|
|
307
|
+
if (!this.authState) return null;
|
|
308
|
+
return {
|
|
309
|
+
userId: this.authState.userId,
|
|
310
|
+
userName: this.authState.userName
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
// --- Local mode (original implementation) ---
|
|
314
|
+
async localLogin(credentials) {
|
|
315
|
+
const { items } = await this.list({ filter: { email: credentials.email } });
|
|
316
|
+
if (items.length === 0) {
|
|
317
|
+
throw new NotFoundError("Invalid email or password");
|
|
318
|
+
}
|
|
319
|
+
const user = items[0];
|
|
320
|
+
if (user.password !== credentials.password) {
|
|
321
|
+
throw new ForbiddenError("Invalid email or password");
|
|
322
|
+
}
|
|
323
|
+
this.authState = {
|
|
324
|
+
userId: user.id,
|
|
325
|
+
email: user.email,
|
|
326
|
+
userName: user.name || user.email,
|
|
327
|
+
role: user.role,
|
|
328
|
+
token: this.generateToken(user)
|
|
329
|
+
};
|
|
330
|
+
this.persistState();
|
|
331
|
+
return user;
|
|
332
|
+
}
|
|
333
|
+
async localRegister(data) {
|
|
334
|
+
const email = data.email;
|
|
335
|
+
if (email) {
|
|
336
|
+
const { items } = await this.list({ filter: { email } });
|
|
337
|
+
if (items.length > 0) {
|
|
338
|
+
throw new ConflictError("Email already registered");
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
const { data: user } = await this.create(data);
|
|
342
|
+
const u = user;
|
|
343
|
+
this.authState = {
|
|
344
|
+
userId: u.id,
|
|
345
|
+
email: u.email,
|
|
346
|
+
userName: u.name || u.email,
|
|
347
|
+
role: u.role,
|
|
348
|
+
token: this.generateToken(u)
|
|
349
|
+
};
|
|
350
|
+
this.persistState();
|
|
351
|
+
return user;
|
|
352
|
+
}
|
|
353
|
+
// --- HTTP mode ---
|
|
354
|
+
async httpLogin(credentials) {
|
|
355
|
+
const preset = this.httpDriver.preset;
|
|
356
|
+
const baseUrl = this.httpDriver.baseUrl;
|
|
357
|
+
const url = `${baseUrl}${preset.auth.loginUrl}`;
|
|
358
|
+
const response = await fetch(url, {
|
|
359
|
+
method: "POST",
|
|
360
|
+
headers: { "Content-Type": "application/json" },
|
|
361
|
+
body: JSON.stringify(credentials)
|
|
362
|
+
});
|
|
363
|
+
if (!response.ok) {
|
|
364
|
+
const body2 = await response.json().catch(() => ({}));
|
|
365
|
+
if (response.status === 401 || response.status === 403) {
|
|
366
|
+
throw new ForbiddenError(body2.message ?? "Invalid email or password");
|
|
367
|
+
}
|
|
368
|
+
if (response.status === 404) {
|
|
369
|
+
throw new NotFoundError(body2.message ?? "Invalid email or password");
|
|
370
|
+
}
|
|
371
|
+
throw new ForbiddenError(body2.message ?? "Login failed");
|
|
372
|
+
}
|
|
373
|
+
const body = await response.json();
|
|
374
|
+
const token = body[preset.auth.tokenField];
|
|
375
|
+
const user = body[preset.auth.userField] ?? body;
|
|
376
|
+
this.authState = {
|
|
377
|
+
userId: user.id,
|
|
378
|
+
email: user.email ?? credentials.email,
|
|
379
|
+
userName: user.name || user.email || credentials.email,
|
|
380
|
+
role: user.role,
|
|
381
|
+
token
|
|
382
|
+
};
|
|
383
|
+
this.persistState();
|
|
384
|
+
return user;
|
|
385
|
+
}
|
|
386
|
+
async httpRegister(data) {
|
|
387
|
+
const preset = this.httpDriver.preset;
|
|
388
|
+
const baseUrl = this.httpDriver.baseUrl;
|
|
389
|
+
const url = `${baseUrl}${preset.auth.registerUrl}`;
|
|
390
|
+
const response = await fetch(url, {
|
|
391
|
+
method: "POST",
|
|
392
|
+
headers: { "Content-Type": "application/json" },
|
|
393
|
+
body: JSON.stringify(data)
|
|
394
|
+
});
|
|
395
|
+
if (!response.ok) {
|
|
396
|
+
const body2 = await response.json().catch(() => ({}));
|
|
397
|
+
if (response.status === 409) {
|
|
398
|
+
throw new ConflictError(body2.message ?? "Email already registered");
|
|
399
|
+
}
|
|
400
|
+
throw new ForbiddenError(body2.message ?? "Registration failed");
|
|
401
|
+
}
|
|
402
|
+
const body = await response.json();
|
|
403
|
+
const token = body[preset.auth.tokenField];
|
|
404
|
+
const user = body[preset.auth.userField] ?? body;
|
|
405
|
+
this.authState = {
|
|
406
|
+
userId: user.id,
|
|
407
|
+
email: user.email ?? data.email,
|
|
408
|
+
userName: user.name || user.email || data.email,
|
|
409
|
+
role: user.role,
|
|
410
|
+
token
|
|
411
|
+
};
|
|
412
|
+
this.persistState();
|
|
413
|
+
return user;
|
|
414
|
+
}
|
|
415
|
+
generateToken(user) {
|
|
416
|
+
return btoa(JSON.stringify({
|
|
417
|
+
userId: user.id,
|
|
418
|
+
email: user.email,
|
|
419
|
+
role: user.role,
|
|
420
|
+
iat: Date.now(),
|
|
421
|
+
exp: Date.now() + 24 * 60 * 60 * 1e3
|
|
422
|
+
}));
|
|
423
|
+
}
|
|
424
|
+
persistState() {
|
|
425
|
+
if (this.saveState) {
|
|
426
|
+
this.saveState(this.authState);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
|
|
205
431
|
// src/query-engine.ts
|
|
206
432
|
var OPERATORS = [
|
|
207
433
|
"startswith",
|
|
@@ -390,11 +616,143 @@ var LocalStorageBackend = class {
|
|
|
390
616
|
localStorage.setItem(`${LS_META_PREFIX}${key}`, value);
|
|
391
617
|
}
|
|
392
618
|
};
|
|
619
|
+
function idbRequest(req) {
|
|
620
|
+
return new Promise((resolve, reject) => {
|
|
621
|
+
req.onsuccess = () => resolve(req.result);
|
|
622
|
+
req.onerror = () => reject(req.error);
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
var IDB_DATA_STORE = "data";
|
|
626
|
+
var IDB_META_STORE = "meta";
|
|
627
|
+
var IndexedDBBackend = class {
|
|
628
|
+
cache = new MemoryStorage();
|
|
629
|
+
db = null;
|
|
630
|
+
_ready;
|
|
631
|
+
constructor(dbName) {
|
|
632
|
+
this._ready = this.open(dbName);
|
|
633
|
+
}
|
|
634
|
+
get ready() {
|
|
635
|
+
return this._ready;
|
|
636
|
+
}
|
|
637
|
+
open(dbName) {
|
|
638
|
+
return new Promise((resolve, reject) => {
|
|
639
|
+
const req = indexedDB.open(dbName, 1);
|
|
640
|
+
req.onupgradeneeded = () => {
|
|
641
|
+
const db = req.result;
|
|
642
|
+
if (!db.objectStoreNames.contains(IDB_DATA_STORE)) {
|
|
643
|
+
db.createObjectStore(IDB_DATA_STORE);
|
|
644
|
+
}
|
|
645
|
+
if (!db.objectStoreNames.contains(IDB_META_STORE)) {
|
|
646
|
+
db.createObjectStore(IDB_META_STORE);
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
req.onsuccess = async () => {
|
|
650
|
+
this.db = req.result;
|
|
651
|
+
await this.loadAll();
|
|
652
|
+
resolve();
|
|
653
|
+
};
|
|
654
|
+
req.onerror = () => reject(req.error);
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
async loadAll() {
|
|
658
|
+
const db = this.db;
|
|
659
|
+
const tx = db.transaction([IDB_DATA_STORE, IDB_META_STORE], "readonly");
|
|
660
|
+
const dataStore = tx.objectStore(IDB_DATA_STORE);
|
|
661
|
+
const metaStore = tx.objectStore(IDB_META_STORE);
|
|
662
|
+
const dataKeys = await idbRequest(dataStore.getAllKeys());
|
|
663
|
+
const dataValues = await idbRequest(dataStore.getAll());
|
|
664
|
+
for (let i = 0; i < dataKeys.length; i++) {
|
|
665
|
+
const compositeKey = dataKeys[i];
|
|
666
|
+
const sepIdx = compositeKey.indexOf(":");
|
|
667
|
+
const resource = compositeKey.substring(0, sepIdx);
|
|
668
|
+
const id = compositeKey.substring(sepIdx + 1);
|
|
669
|
+
this.cache.set(resource, id, dataValues[i]);
|
|
670
|
+
}
|
|
671
|
+
const metaKeys = await idbRequest(metaStore.getAllKeys());
|
|
672
|
+
const metaValues = await idbRequest(metaStore.getAll());
|
|
673
|
+
for (let i = 0; i < metaKeys.length; i++) {
|
|
674
|
+
this.cache.setMeta(metaKeys[i], metaValues[i]);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
writeData(resource, id, data) {
|
|
678
|
+
if (!this.db) return;
|
|
679
|
+
const tx = this.db.transaction(IDB_DATA_STORE, "readwrite");
|
|
680
|
+
tx.objectStore(IDB_DATA_STORE).put(data, `${resource}:${id}`);
|
|
681
|
+
}
|
|
682
|
+
deleteData(resource, id) {
|
|
683
|
+
if (!this.db) return;
|
|
684
|
+
const tx = this.db.transaction(IDB_DATA_STORE, "readwrite");
|
|
685
|
+
tx.objectStore(IDB_DATA_STORE).delete(`${resource}:${id}`);
|
|
686
|
+
}
|
|
687
|
+
writeMeta(key, value) {
|
|
688
|
+
if (!this.db) return;
|
|
689
|
+
const tx = this.db.transaction(IDB_META_STORE, "readwrite");
|
|
690
|
+
tx.objectStore(IDB_META_STORE).put(value, key);
|
|
691
|
+
}
|
|
692
|
+
getAll(resource) {
|
|
693
|
+
return this.cache.getAll(resource);
|
|
694
|
+
}
|
|
695
|
+
getById(resource, id) {
|
|
696
|
+
return this.cache.getById(resource, id);
|
|
697
|
+
}
|
|
698
|
+
set(resource, id, data) {
|
|
699
|
+
this.cache.set(resource, id, data);
|
|
700
|
+
this.writeData(resource, id, data);
|
|
701
|
+
}
|
|
702
|
+
remove(resource, id) {
|
|
703
|
+
this.cache.remove(resource, id);
|
|
704
|
+
this.deleteData(resource, id);
|
|
705
|
+
}
|
|
706
|
+
clear(resource) {
|
|
707
|
+
const items = this.cache.getAll(resource);
|
|
708
|
+
this.cache.clear(resource);
|
|
709
|
+
if (this.db) {
|
|
710
|
+
const tx = this.db.transaction(IDB_DATA_STORE, "readwrite");
|
|
711
|
+
const store = tx.objectStore(IDB_DATA_STORE);
|
|
712
|
+
for (const item of items) {
|
|
713
|
+
store.delete(`${resource}:${item.id}`);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
getMeta(key) {
|
|
718
|
+
return this.cache.getMeta(key);
|
|
719
|
+
}
|
|
720
|
+
setMeta(key, value) {
|
|
721
|
+
this.cache.setMeta(key, value);
|
|
722
|
+
this.writeMeta(key, value);
|
|
723
|
+
}
|
|
724
|
+
};
|
|
393
725
|
var LocalDriver = class {
|
|
394
726
|
storage;
|
|
395
727
|
entityClasses = /* @__PURE__ */ new Map();
|
|
728
|
+
authProvider = null;
|
|
729
|
+
_ready;
|
|
730
|
+
_isReady;
|
|
396
731
|
constructor(config) {
|
|
397
|
-
|
|
732
|
+
if (config.persist === "indexeddb") {
|
|
733
|
+
const backend = new IndexedDBBackend(config.dbName ?? "fauxbase");
|
|
734
|
+
this.storage = backend;
|
|
735
|
+
this._isReady = false;
|
|
736
|
+
this._ready = backend.ready.then(() => {
|
|
737
|
+
this._isReady = true;
|
|
738
|
+
});
|
|
739
|
+
} else {
|
|
740
|
+
this.storage = config.persist === "localStorage" ? new LocalStorageBackend() : new MemoryStorage();
|
|
741
|
+
this._isReady = true;
|
|
742
|
+
this._ready = Promise.resolve();
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
get ready() {
|
|
746
|
+
return this._ready;
|
|
747
|
+
}
|
|
748
|
+
get isReady() {
|
|
749
|
+
return this._isReady;
|
|
750
|
+
}
|
|
751
|
+
setAuthProvider(provider) {
|
|
752
|
+
this.authProvider = provider;
|
|
753
|
+
}
|
|
754
|
+
getStorageBackend() {
|
|
755
|
+
return this.storage;
|
|
398
756
|
}
|
|
399
757
|
registerEntity(resource, entityClass) {
|
|
400
758
|
this.entityClasses.set(resource, entityClass);
|
|
@@ -417,13 +775,20 @@ var LocalDriver = class {
|
|
|
417
775
|
async create(resource, data) {
|
|
418
776
|
const entityClass = this.entityClasses.get(resource);
|
|
419
777
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
778
|
+
const authContext = this.authProvider?.();
|
|
420
779
|
let record = {
|
|
421
780
|
...data,
|
|
422
781
|
id: data.id || generateUUID(),
|
|
423
782
|
createdAt: now,
|
|
424
783
|
updatedAt: now,
|
|
425
784
|
deletedAt: null,
|
|
426
|
-
version: 1
|
|
785
|
+
version: 1,
|
|
786
|
+
...authContext ? {
|
|
787
|
+
createdById: authContext.userId,
|
|
788
|
+
createdByName: authContext.userName,
|
|
789
|
+
updatedById: authContext.userId,
|
|
790
|
+
updatedByName: authContext.userName
|
|
791
|
+
} : {}
|
|
427
792
|
};
|
|
428
793
|
if (entityClass) {
|
|
429
794
|
record = applyDefaults(record, entityClass);
|
|
@@ -442,13 +807,18 @@ var LocalDriver = class {
|
|
|
442
807
|
if (entityClass) {
|
|
443
808
|
validateEntity(data, entityClass, false);
|
|
444
809
|
}
|
|
810
|
+
const authContext = this.authProvider?.();
|
|
445
811
|
const record = {
|
|
446
812
|
...existing,
|
|
447
813
|
...data,
|
|
448
814
|
id,
|
|
449
815
|
createdAt: existing.createdAt,
|
|
450
816
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
451
|
-
version: (existing.version || 0) + 1
|
|
817
|
+
version: (existing.version || 0) + 1,
|
|
818
|
+
...authContext ? {
|
|
819
|
+
updatedById: authContext.userId,
|
|
820
|
+
updatedByName: authContext.userName
|
|
821
|
+
} : {}
|
|
452
822
|
};
|
|
453
823
|
this.storage.set(resource, id, record);
|
|
454
824
|
const result = entityClass ? applyComputedFields(record, entityClass) : record;
|
|
@@ -460,11 +830,18 @@ var LocalDriver = class {
|
|
|
460
830
|
throw new NotFoundError(`${resource} with id "${id}" not found`);
|
|
461
831
|
}
|
|
462
832
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
833
|
+
const authContext = this.authProvider?.();
|
|
463
834
|
const record = {
|
|
464
835
|
...existing,
|
|
465
836
|
deletedAt: now,
|
|
466
837
|
updatedAt: now,
|
|
467
|
-
version: (existing.version || 0) + 1
|
|
838
|
+
version: (existing.version || 0) + 1,
|
|
839
|
+
...authContext ? {
|
|
840
|
+
deletedById: authContext.userId,
|
|
841
|
+
deletedByName: authContext.userName,
|
|
842
|
+
updatedById: authContext.userId,
|
|
843
|
+
updatedByName: authContext.userName
|
|
844
|
+
} : {}
|
|
468
845
|
};
|
|
469
846
|
this.storage.set(resource, id, record);
|
|
470
847
|
return { data: record };
|
|
@@ -527,6 +904,631 @@ var LocalDriver = class {
|
|
|
527
904
|
}
|
|
528
905
|
};
|
|
529
906
|
|
|
907
|
+
// src/presets/types.ts
|
|
908
|
+
function definePreset(config) {
|
|
909
|
+
return config;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// src/presets/default.ts
|
|
913
|
+
var defaultPreset = definePreset({
|
|
914
|
+
name: "default",
|
|
915
|
+
response: {
|
|
916
|
+
single: (raw) => ({ data: raw.data ?? raw }),
|
|
917
|
+
list: (raw) => ({
|
|
918
|
+
items: raw.items ?? raw.data ?? [],
|
|
919
|
+
meta: raw.meta ?? {}
|
|
920
|
+
}),
|
|
921
|
+
error: (raw) => ({
|
|
922
|
+
error: raw.error ?? raw.message ?? "Unknown error",
|
|
923
|
+
code: raw.code ?? "UNKNOWN",
|
|
924
|
+
details: raw.details
|
|
925
|
+
})
|
|
926
|
+
},
|
|
927
|
+
meta: { page: "page", size: "size", totalItems: "totalItems", totalPages: "totalPages" },
|
|
928
|
+
query: {
|
|
929
|
+
filterStyle: "django",
|
|
930
|
+
pageParam: "page",
|
|
931
|
+
sizeParam: "size",
|
|
932
|
+
sortParam: "sort",
|
|
933
|
+
sortFormat: "field,direction"
|
|
934
|
+
},
|
|
935
|
+
auth: {
|
|
936
|
+
loginUrl: "/auth/login",
|
|
937
|
+
registerUrl: "/auth/register",
|
|
938
|
+
logoutUrl: "/auth/logout",
|
|
939
|
+
tokenField: "token",
|
|
940
|
+
userField: "user",
|
|
941
|
+
headerFormat: "Bearer {token}"
|
|
942
|
+
}
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
// src/presets/spring-boot.ts
|
|
946
|
+
var springBootPreset = definePreset({
|
|
947
|
+
name: "spring-boot",
|
|
948
|
+
response: {
|
|
949
|
+
single: (raw) => ({ data: raw }),
|
|
950
|
+
list: (raw) => ({
|
|
951
|
+
items: raw.content ?? [],
|
|
952
|
+
meta: {
|
|
953
|
+
page: (raw.pageable?.pageNumber ?? 0) + 1,
|
|
954
|
+
size: raw.pageable?.pageSize ?? raw.size ?? 20,
|
|
955
|
+
totalItems: raw.totalElements ?? 0,
|
|
956
|
+
totalPages: raw.totalPages ?? 0
|
|
957
|
+
}
|
|
958
|
+
}),
|
|
959
|
+
error: (raw) => ({
|
|
960
|
+
error: raw.message ?? raw.error ?? "Unknown error",
|
|
961
|
+
code: raw.status?.toString() ?? "UNKNOWN",
|
|
962
|
+
details: raw.errors?.reduce?.((acc, e) => {
|
|
963
|
+
acc[e.field ?? e.code ?? "unknown"] = e.defaultMessage ?? e.message ?? "";
|
|
964
|
+
return acc;
|
|
965
|
+
}, {}) ?? void 0
|
|
966
|
+
})
|
|
967
|
+
},
|
|
968
|
+
meta: { page: "page", size: "size", totalItems: "totalElements", totalPages: "totalPages" },
|
|
969
|
+
query: {
|
|
970
|
+
filterStyle: "dot",
|
|
971
|
+
pageParam: "page",
|
|
972
|
+
sizeParam: "size",
|
|
973
|
+
sortParam: "sort",
|
|
974
|
+
sortFormat: "field,direction",
|
|
975
|
+
pageOffset: -1
|
|
976
|
+
// 0-indexed
|
|
977
|
+
},
|
|
978
|
+
auth: {
|
|
979
|
+
loginUrl: "/api/auth/login",
|
|
980
|
+
registerUrl: "/api/auth/register",
|
|
981
|
+
tokenField: "token",
|
|
982
|
+
userField: "user",
|
|
983
|
+
headerFormat: "Bearer {token}"
|
|
984
|
+
}
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
// src/presets/laravel.ts
|
|
988
|
+
var laravelPreset = definePreset({
|
|
989
|
+
name: "laravel",
|
|
990
|
+
response: {
|
|
991
|
+
single: (raw) => ({ data: raw.data ?? raw }),
|
|
992
|
+
list: (raw) => ({
|
|
993
|
+
items: raw.data ?? [],
|
|
994
|
+
meta: {
|
|
995
|
+
page: raw.meta?.current_page ?? raw.current_page ?? 1,
|
|
996
|
+
size: raw.meta?.per_page ?? raw.per_page ?? 15,
|
|
997
|
+
totalItems: raw.meta?.total ?? raw.total ?? 0,
|
|
998
|
+
totalPages: raw.meta?.last_page ?? raw.last_page ?? 0
|
|
999
|
+
}
|
|
1000
|
+
}),
|
|
1001
|
+
error: (raw) => ({
|
|
1002
|
+
error: raw.message ?? "Unknown error",
|
|
1003
|
+
code: raw.status?.toString() ?? "UNKNOWN",
|
|
1004
|
+
details: raw.errors ? Object.entries(raw.errors).reduce((acc, [key, val]) => {
|
|
1005
|
+
acc[key] = Array.isArray(val) ? val[0] : val;
|
|
1006
|
+
return acc;
|
|
1007
|
+
}, {}) : void 0
|
|
1008
|
+
})
|
|
1009
|
+
},
|
|
1010
|
+
meta: { page: "current_page", size: "per_page", totalItems: "total", totalPages: "last_page" },
|
|
1011
|
+
query: {
|
|
1012
|
+
filterStyle: "bracket",
|
|
1013
|
+
pageParam: "page",
|
|
1014
|
+
sizeParam: "per_page",
|
|
1015
|
+
sortParam: "sort",
|
|
1016
|
+
sortFormat: "field,direction"
|
|
1017
|
+
},
|
|
1018
|
+
auth: {
|
|
1019
|
+
loginUrl: "/api/login",
|
|
1020
|
+
registerUrl: "/api/register",
|
|
1021
|
+
logoutUrl: "/api/logout",
|
|
1022
|
+
tokenField: "token",
|
|
1023
|
+
userField: "user",
|
|
1024
|
+
headerFormat: "Bearer {token}"
|
|
1025
|
+
}
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
// src/presets/django.ts
|
|
1029
|
+
var djangoPreset = definePreset({
|
|
1030
|
+
name: "django",
|
|
1031
|
+
response: {
|
|
1032
|
+
single: (raw) => ({ data: raw }),
|
|
1033
|
+
list: (raw) => ({
|
|
1034
|
+
items: raw.results ?? [],
|
|
1035
|
+
meta: {
|
|
1036
|
+
page: 1,
|
|
1037
|
+
// Django REST doesn't always include page number
|
|
1038
|
+
size: raw.results?.length ?? 0,
|
|
1039
|
+
totalItems: raw.count ?? 0,
|
|
1040
|
+
totalPages: raw.count && raw.results?.length ? Math.ceil(raw.count / raw.results.length) : 0
|
|
1041
|
+
}
|
|
1042
|
+
}),
|
|
1043
|
+
error: (raw) => ({
|
|
1044
|
+
error: raw.detail ?? raw.message ?? "Unknown error",
|
|
1045
|
+
code: raw.status_code?.toString() ?? "UNKNOWN",
|
|
1046
|
+
details: typeof raw === "object" && !raw.detail ? Object.entries(raw).reduce((acc, [key, val]) => {
|
|
1047
|
+
if (key !== "status_code") {
|
|
1048
|
+
acc[key] = Array.isArray(val) ? val[0] : String(val);
|
|
1049
|
+
}
|
|
1050
|
+
return acc;
|
|
1051
|
+
}, {}) : void 0
|
|
1052
|
+
})
|
|
1053
|
+
},
|
|
1054
|
+
meta: { page: "page", size: "page_size", totalItems: "count", totalPages: "total_pages" },
|
|
1055
|
+
query: {
|
|
1056
|
+
filterStyle: "django",
|
|
1057
|
+
pageParam: "page",
|
|
1058
|
+
sizeParam: "page_size",
|
|
1059
|
+
sortParam: "ordering",
|
|
1060
|
+
sortFormat: "field,direction"
|
|
1061
|
+
},
|
|
1062
|
+
auth: {
|
|
1063
|
+
loginUrl: "/api/auth/login/",
|
|
1064
|
+
registerUrl: "/api/auth/register/",
|
|
1065
|
+
logoutUrl: "/api/auth/logout/",
|
|
1066
|
+
tokenField: "token",
|
|
1067
|
+
userField: "user",
|
|
1068
|
+
headerFormat: "Token {token}"
|
|
1069
|
+
}
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
// src/presets/nestjs.ts
|
|
1073
|
+
var nestjsPreset = definePreset({
|
|
1074
|
+
name: "nestjs",
|
|
1075
|
+
response: {
|
|
1076
|
+
single: (raw) => ({ data: raw.data ?? raw }),
|
|
1077
|
+
list: (raw) => ({
|
|
1078
|
+
items: raw.data ?? raw.items ?? [],
|
|
1079
|
+
meta: raw.meta ?? {}
|
|
1080
|
+
}),
|
|
1081
|
+
error: (raw) => ({
|
|
1082
|
+
error: raw.message ?? "Unknown error",
|
|
1083
|
+
code: raw.error ?? raw.statusCode?.toString() ?? "UNKNOWN",
|
|
1084
|
+
details: raw.message && Array.isArray(raw.message) ? raw.message.reduce((acc, msg, i) => {
|
|
1085
|
+
acc[`field_${i}`] = msg;
|
|
1086
|
+
return acc;
|
|
1087
|
+
}, {}) : void 0
|
|
1088
|
+
})
|
|
1089
|
+
},
|
|
1090
|
+
meta: { page: "page", size: "limit", totalItems: "totalItems", totalPages: "totalPages" },
|
|
1091
|
+
query: {
|
|
1092
|
+
filterStyle: "nestjs",
|
|
1093
|
+
pageParam: "page",
|
|
1094
|
+
sizeParam: "limit",
|
|
1095
|
+
sortParam: "sort",
|
|
1096
|
+
sortFormat: "field:direction"
|
|
1097
|
+
},
|
|
1098
|
+
auth: {
|
|
1099
|
+
loginUrl: "/auth/login",
|
|
1100
|
+
registerUrl: "/auth/register",
|
|
1101
|
+
tokenField: "access_token",
|
|
1102
|
+
userField: "user",
|
|
1103
|
+
headerFormat: "Bearer {token}"
|
|
1104
|
+
}
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
// src/presets/express.ts
|
|
1108
|
+
var expressPreset = definePreset({
|
|
1109
|
+
name: "express",
|
|
1110
|
+
response: {
|
|
1111
|
+
single: (raw) => ({ data: raw.data ?? raw }),
|
|
1112
|
+
list: (raw) => ({
|
|
1113
|
+
items: raw.data ?? raw.items ?? [],
|
|
1114
|
+
meta: raw.meta ?? {}
|
|
1115
|
+
}),
|
|
1116
|
+
error: (raw) => ({
|
|
1117
|
+
error: raw.error ?? raw.message ?? "Unknown error",
|
|
1118
|
+
code: raw.code ?? "UNKNOWN",
|
|
1119
|
+
details: raw.details
|
|
1120
|
+
})
|
|
1121
|
+
},
|
|
1122
|
+
meta: { page: "page", size: "size", totalItems: "totalItems", totalPages: "totalPages" },
|
|
1123
|
+
query: {
|
|
1124
|
+
filterStyle: "django",
|
|
1125
|
+
pageParam: "page",
|
|
1126
|
+
sizeParam: "size",
|
|
1127
|
+
sortParam: "sort",
|
|
1128
|
+
sortFormat: "field,direction"
|
|
1129
|
+
},
|
|
1130
|
+
auth: {
|
|
1131
|
+
loginUrl: "/api/auth/login",
|
|
1132
|
+
registerUrl: "/api/auth/register",
|
|
1133
|
+
logoutUrl: "/api/auth/logout",
|
|
1134
|
+
tokenField: "token",
|
|
1135
|
+
userField: "user",
|
|
1136
|
+
headerFormat: "Bearer {token}"
|
|
1137
|
+
}
|
|
1138
|
+
});
|
|
1139
|
+
|
|
1140
|
+
// src/presets/index.ts
|
|
1141
|
+
var presetRegistry = /* @__PURE__ */ new Map([
|
|
1142
|
+
["default", defaultPreset],
|
|
1143
|
+
["spring-boot", springBootPreset],
|
|
1144
|
+
["laravel", laravelPreset],
|
|
1145
|
+
["django", djangoPreset],
|
|
1146
|
+
["nestjs", nestjsPreset],
|
|
1147
|
+
["express", expressPreset]
|
|
1148
|
+
]);
|
|
1149
|
+
function getPreset(name) {
|
|
1150
|
+
const preset = presetRegistry.get(name);
|
|
1151
|
+
if (!preset) {
|
|
1152
|
+
throw new Error(`Unknown preset: "${name}". Available: ${Array.from(presetRegistry.keys()).join(", ")}`);
|
|
1153
|
+
}
|
|
1154
|
+
return preset;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// src/drivers/query-serializer.ts
|
|
1158
|
+
function serializeQuery(query, config) {
|
|
1159
|
+
const params = new URLSearchParams();
|
|
1160
|
+
if (query.filter) {
|
|
1161
|
+
serializeFilters(params, query.filter, config.filterStyle);
|
|
1162
|
+
}
|
|
1163
|
+
if (query.sort && config.sortParam) {
|
|
1164
|
+
const direction = query.sort.direction;
|
|
1165
|
+
if (config.sortParam === "ordering") {
|
|
1166
|
+
params.set(config.sortParam, direction === "desc" ? `-${query.sort.field}` : query.sort.field);
|
|
1167
|
+
} else if (config.sortFormat === "field:direction") {
|
|
1168
|
+
params.set(config.sortParam, `${query.sort.field}:${direction}`);
|
|
1169
|
+
} else {
|
|
1170
|
+
params.set(config.sortParam, `${query.sort.field},${direction}`);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
if (query.page !== void 0) {
|
|
1174
|
+
const pageOffset = config.pageOffset ?? 0;
|
|
1175
|
+
params.set(config.pageParam, String(query.page + pageOffset));
|
|
1176
|
+
}
|
|
1177
|
+
if (query.size !== void 0) {
|
|
1178
|
+
params.set(config.sizeParam, String(query.size));
|
|
1179
|
+
}
|
|
1180
|
+
return params;
|
|
1181
|
+
}
|
|
1182
|
+
function serializeFilters(params, filter, style) {
|
|
1183
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
1184
|
+
if (value === void 0) continue;
|
|
1185
|
+
const serialized = typeof value === "object" && !Array.isArray(value) ? JSON.stringify(value) : String(value);
|
|
1186
|
+
switch (style) {
|
|
1187
|
+
case "django":
|
|
1188
|
+
params.set(key, serialized);
|
|
1189
|
+
break;
|
|
1190
|
+
case "dot":
|
|
1191
|
+
params.set(key.replace(/__/g, "."), serialized);
|
|
1192
|
+
break;
|
|
1193
|
+
case "bracket": {
|
|
1194
|
+
const bracketKey = key.replace(/__/g, "_");
|
|
1195
|
+
params.set(`filter[${bracketKey}]`, serialized);
|
|
1196
|
+
break;
|
|
1197
|
+
}
|
|
1198
|
+
case "nestjs": {
|
|
1199
|
+
const parts = key.split("__");
|
|
1200
|
+
if (parts.length === 2) {
|
|
1201
|
+
params.set(`filter.${parts[0]}.$${parts[1]}`, serialized);
|
|
1202
|
+
} else {
|
|
1203
|
+
params.set(`filter.${key}`, serialized);
|
|
1204
|
+
}
|
|
1205
|
+
break;
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
// src/drivers/http.ts
|
|
1212
|
+
var HttpDriver = class {
|
|
1213
|
+
baseUrl;
|
|
1214
|
+
preset;
|
|
1215
|
+
timeout;
|
|
1216
|
+
maxRetries;
|
|
1217
|
+
baseDelay;
|
|
1218
|
+
defaultHeaders;
|
|
1219
|
+
endpoints = /* @__PURE__ */ new Map();
|
|
1220
|
+
authProvider = null;
|
|
1221
|
+
constructor(config) {
|
|
1222
|
+
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
1223
|
+
this.preset = typeof config.preset === "string" ? getPreset(config.preset ?? "default") : config.preset ?? getPreset("default");
|
|
1224
|
+
this.timeout = config.timeout ?? 3e4;
|
|
1225
|
+
this.maxRetries = config.retry?.maxRetries ?? 3;
|
|
1226
|
+
this.baseDelay = config.retry?.baseDelay ?? 300;
|
|
1227
|
+
this.defaultHeaders = config.headers ?? {};
|
|
1228
|
+
}
|
|
1229
|
+
setAuthProvider(provider) {
|
|
1230
|
+
this.authProvider = provider;
|
|
1231
|
+
}
|
|
1232
|
+
registerEndpoint(resource, endpoint) {
|
|
1233
|
+
this.endpoints.set(resource, endpoint);
|
|
1234
|
+
}
|
|
1235
|
+
getEndpoint(resource) {
|
|
1236
|
+
return this.endpoints.get(resource) ?? `/${resource}`;
|
|
1237
|
+
}
|
|
1238
|
+
buildUrl(resource, id) {
|
|
1239
|
+
const endpoint = this.getEndpoint(resource);
|
|
1240
|
+
const base = `${this.baseUrl}${endpoint}`;
|
|
1241
|
+
return id ? `${base}/${id}` : base;
|
|
1242
|
+
}
|
|
1243
|
+
buildHeaders() {
|
|
1244
|
+
const headers = {
|
|
1245
|
+
"Content-Type": "application/json",
|
|
1246
|
+
...this.defaultHeaders
|
|
1247
|
+
};
|
|
1248
|
+
const auth = this.authProvider?.();
|
|
1249
|
+
if (auth?.token) {
|
|
1250
|
+
headers["Authorization"] = this.preset.auth.headerFormat.replace("{token}", auth.token);
|
|
1251
|
+
}
|
|
1252
|
+
return headers;
|
|
1253
|
+
}
|
|
1254
|
+
async request(url, options = {}, retryCount = 0) {
|
|
1255
|
+
const controller = new AbortController();
|
|
1256
|
+
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
1257
|
+
try {
|
|
1258
|
+
const response = await fetch(url, {
|
|
1259
|
+
...options,
|
|
1260
|
+
headers: { ...this.buildHeaders(), ...options.headers ?? {} },
|
|
1261
|
+
signal: controller.signal
|
|
1262
|
+
});
|
|
1263
|
+
clearTimeout(timer);
|
|
1264
|
+
if (!response.ok) {
|
|
1265
|
+
if (response.status >= 500 && retryCount < this.maxRetries) {
|
|
1266
|
+
const delay = this.baseDelay * Math.pow(2, retryCount);
|
|
1267
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
1268
|
+
return this.request(url, options, retryCount + 1);
|
|
1269
|
+
}
|
|
1270
|
+
const body = await response.json().catch(() => ({}));
|
|
1271
|
+
this.throwMappedError(response.status, body);
|
|
1272
|
+
}
|
|
1273
|
+
if (response.status === 204) {
|
|
1274
|
+
return {};
|
|
1275
|
+
}
|
|
1276
|
+
return response.json();
|
|
1277
|
+
} catch (err) {
|
|
1278
|
+
clearTimeout(timer);
|
|
1279
|
+
if (err instanceof FauxbaseError) throw err;
|
|
1280
|
+
if (err.name === "AbortError") {
|
|
1281
|
+
throw new TimeoutError(`Request timed out after ${this.timeout}ms`);
|
|
1282
|
+
}
|
|
1283
|
+
throw new NetworkError(err.message ?? "Network request failed");
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
throwMappedError(status, body) {
|
|
1287
|
+
const parsed = this.preset.response.error(body);
|
|
1288
|
+
switch (true) {
|
|
1289
|
+
case (status === 400 || status === 422):
|
|
1290
|
+
throw new ValidationError(parsed.error, parsed.details);
|
|
1291
|
+
case (status === 401 || status === 403):
|
|
1292
|
+
throw new ForbiddenError(parsed.error);
|
|
1293
|
+
case status === 404:
|
|
1294
|
+
throw new NotFoundError(parsed.error);
|
|
1295
|
+
case status === 409:
|
|
1296
|
+
throw new ConflictError(parsed.error);
|
|
1297
|
+
default:
|
|
1298
|
+
throw new HttpError(parsed.error, status, parsed.details);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
async list(resource, query) {
|
|
1302
|
+
const url = this.buildUrl(resource);
|
|
1303
|
+
const params = serializeQuery(query, this.preset.query);
|
|
1304
|
+
const queryString = params.toString();
|
|
1305
|
+
const fullUrl = queryString ? `${url}?${queryString}` : url;
|
|
1306
|
+
const raw = await this.request(fullUrl);
|
|
1307
|
+
const parsed = this.preset.response.list(raw);
|
|
1308
|
+
return {
|
|
1309
|
+
items: parsed.items,
|
|
1310
|
+
meta: {
|
|
1311
|
+
page: parsed.meta[this.preset.meta.page] ?? parsed.meta.page ?? query.page ?? 1,
|
|
1312
|
+
size: parsed.meta[this.preset.meta.size] ?? parsed.meta.size ?? query.size ?? 20,
|
|
1313
|
+
totalItems: parsed.meta[this.preset.meta.totalItems] ?? parsed.meta.totalItems ?? 0,
|
|
1314
|
+
totalPages: parsed.meta[this.preset.meta.totalPages] ?? parsed.meta.totalPages ?? 0
|
|
1315
|
+
}
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
async get(resource, id) {
|
|
1319
|
+
const url = this.buildUrl(resource, id);
|
|
1320
|
+
const raw = await this.request(url);
|
|
1321
|
+
return this.preset.response.single(raw);
|
|
1322
|
+
}
|
|
1323
|
+
async create(resource, data) {
|
|
1324
|
+
const url = this.buildUrl(resource);
|
|
1325
|
+
const raw = await this.request(url, {
|
|
1326
|
+
method: "POST",
|
|
1327
|
+
body: JSON.stringify(data)
|
|
1328
|
+
});
|
|
1329
|
+
return this.preset.response.single(raw);
|
|
1330
|
+
}
|
|
1331
|
+
async update(resource, id, data) {
|
|
1332
|
+
const url = this.buildUrl(resource, id);
|
|
1333
|
+
const raw = await this.request(url, {
|
|
1334
|
+
method: "PATCH",
|
|
1335
|
+
body: JSON.stringify(data)
|
|
1336
|
+
});
|
|
1337
|
+
return this.preset.response.single(raw);
|
|
1338
|
+
}
|
|
1339
|
+
async delete(resource, id) {
|
|
1340
|
+
const url = this.buildUrl(resource, id);
|
|
1341
|
+
const raw = await this.request(url, {
|
|
1342
|
+
method: "DELETE"
|
|
1343
|
+
});
|
|
1344
|
+
return this.preset.response.single(raw);
|
|
1345
|
+
}
|
|
1346
|
+
async count(resource, filter) {
|
|
1347
|
+
const url = `${this.buildUrl(resource)}/count`;
|
|
1348
|
+
const params = filter ? serializeQuery({ filter }, this.preset.query) : new URLSearchParams();
|
|
1349
|
+
const queryString = params.toString();
|
|
1350
|
+
const fullUrl = queryString ? `${url}?${queryString}` : url;
|
|
1351
|
+
const raw = await this.request(fullUrl);
|
|
1352
|
+
return raw.count ?? raw.data?.count ?? 0;
|
|
1353
|
+
}
|
|
1354
|
+
async bulkCreate(resource, data) {
|
|
1355
|
+
const url = `${this.buildUrl(resource)}/bulk`;
|
|
1356
|
+
const raw = await this.request(url, {
|
|
1357
|
+
method: "POST",
|
|
1358
|
+
body: JSON.stringify(data)
|
|
1359
|
+
});
|
|
1360
|
+
const parsed = this.preset.response.single(raw);
|
|
1361
|
+
return { data: Array.isArray(parsed.data) ? parsed.data : [parsed.data] };
|
|
1362
|
+
}
|
|
1363
|
+
async bulkUpdate(resource, updates) {
|
|
1364
|
+
const url = `${this.buildUrl(resource)}/bulk`;
|
|
1365
|
+
const raw = await this.request(url, {
|
|
1366
|
+
method: "PATCH",
|
|
1367
|
+
body: JSON.stringify(updates)
|
|
1368
|
+
});
|
|
1369
|
+
const parsed = this.preset.response.single(raw);
|
|
1370
|
+
return { data: Array.isArray(parsed.data) ? parsed.data : [parsed.data] };
|
|
1371
|
+
}
|
|
1372
|
+
async bulkDelete(resource, ids) {
|
|
1373
|
+
const url = `${this.buildUrl(resource)}/bulk`;
|
|
1374
|
+
const raw = await this.request(url, {
|
|
1375
|
+
method: "DELETE",
|
|
1376
|
+
body: JSON.stringify({ ids })
|
|
1377
|
+
});
|
|
1378
|
+
return { data: { count: raw.count ?? raw.data?.count ?? ids.length } };
|
|
1379
|
+
}
|
|
1380
|
+
// Seed methods are no-ops for HTTP — backend owns data
|
|
1381
|
+
seed() {
|
|
1382
|
+
}
|
|
1383
|
+
getSeedVersion() {
|
|
1384
|
+
return null;
|
|
1385
|
+
}
|
|
1386
|
+
setSeedVersion() {
|
|
1387
|
+
}
|
|
1388
|
+
clear() {
|
|
1389
|
+
}
|
|
1390
|
+
};
|
|
1391
|
+
|
|
1392
|
+
// src/events/event-bus.ts
|
|
1393
|
+
var EventBus = class {
|
|
1394
|
+
listeners = /* @__PURE__ */ new Map();
|
|
1395
|
+
anyListeners = /* @__PURE__ */ new Set();
|
|
1396
|
+
on(resource, handler) {
|
|
1397
|
+
if (!this.listeners.has(resource)) {
|
|
1398
|
+
this.listeners.set(resource, /* @__PURE__ */ new Set());
|
|
1399
|
+
}
|
|
1400
|
+
this.listeners.get(resource).add(handler);
|
|
1401
|
+
return () => {
|
|
1402
|
+
this.listeners.get(resource)?.delete(handler);
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
onAny(handler) {
|
|
1406
|
+
this.anyListeners.add(handler);
|
|
1407
|
+
return () => {
|
|
1408
|
+
this.anyListeners.delete(handler);
|
|
1409
|
+
};
|
|
1410
|
+
}
|
|
1411
|
+
emit(event) {
|
|
1412
|
+
const resourceListeners = this.listeners.get(event.resource);
|
|
1413
|
+
if (resourceListeners) {
|
|
1414
|
+
for (const handler of resourceListeners) {
|
|
1415
|
+
handler(event);
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
for (const handler of this.anyListeners) {
|
|
1419
|
+
handler(event);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
destroy() {
|
|
1423
|
+
this.listeners.clear();
|
|
1424
|
+
this.anyListeners.clear();
|
|
1425
|
+
}
|
|
1426
|
+
};
|
|
1427
|
+
|
|
1428
|
+
// src/events/sse-source.ts
|
|
1429
|
+
var SSESource = class {
|
|
1430
|
+
constructor(config, eventBus) {
|
|
1431
|
+
this.config = config;
|
|
1432
|
+
this.eventBus = eventBus;
|
|
1433
|
+
}
|
|
1434
|
+
eventSource = null;
|
|
1435
|
+
connect() {
|
|
1436
|
+
this.eventSource = new EventSource(this.config.url, {
|
|
1437
|
+
withCredentials: this.config.withCredentials
|
|
1438
|
+
});
|
|
1439
|
+
for (const [eventType, resource] of Object.entries(this.config.eventMap)) {
|
|
1440
|
+
this.eventSource.addEventListener(eventType, (e) => {
|
|
1441
|
+
const parsed = this.parseEvent(e, resource);
|
|
1442
|
+
if (parsed) {
|
|
1443
|
+
this.eventBus.emit(parsed);
|
|
1444
|
+
}
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
disconnect() {
|
|
1449
|
+
if (this.eventSource) {
|
|
1450
|
+
this.eventSource.close();
|
|
1451
|
+
this.eventSource = null;
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
parseEvent(e, resource) {
|
|
1455
|
+
try {
|
|
1456
|
+
const raw = JSON.parse(e.data);
|
|
1457
|
+
return {
|
|
1458
|
+
action: raw.action,
|
|
1459
|
+
resource,
|
|
1460
|
+
data: raw.data,
|
|
1461
|
+
id: raw.id,
|
|
1462
|
+
ids: raw.ids,
|
|
1463
|
+
timestamp: raw.timestamp ?? Date.now(),
|
|
1464
|
+
source: "remote"
|
|
1465
|
+
};
|
|
1466
|
+
} catch {
|
|
1467
|
+
return null;
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
};
|
|
1471
|
+
|
|
1472
|
+
// src/events/stomp-source.ts
|
|
1473
|
+
var STOMPSource = class {
|
|
1474
|
+
constructor(config, eventBus) {
|
|
1475
|
+
this.config = config;
|
|
1476
|
+
this.eventBus = eventBus;
|
|
1477
|
+
}
|
|
1478
|
+
client = null;
|
|
1479
|
+
connect() {
|
|
1480
|
+
this.connectAsync();
|
|
1481
|
+
}
|
|
1482
|
+
async connectAsync() {
|
|
1483
|
+
let StompJs;
|
|
1484
|
+
try {
|
|
1485
|
+
const moduleName = "@stomp/stompjs";
|
|
1486
|
+
StompJs = await Function("m", "return import(m)")(moduleName);
|
|
1487
|
+
} catch {
|
|
1488
|
+
throw new Error(
|
|
1489
|
+
"STOMP source requires @stomp/stompjs. Install it: npm install @stomp/stompjs"
|
|
1490
|
+
);
|
|
1491
|
+
}
|
|
1492
|
+
this.client = new StompJs.Client({
|
|
1493
|
+
brokerURL: this.config.brokerUrl,
|
|
1494
|
+
connectHeaders: this.config.connectHeaders,
|
|
1495
|
+
onConnect: () => {
|
|
1496
|
+
for (const [destination, resource] of Object.entries(this.config.subscriptions)) {
|
|
1497
|
+
this.client.subscribe(destination, (message) => {
|
|
1498
|
+
const parsed = this.parseMessage(message, resource);
|
|
1499
|
+
if (parsed) {
|
|
1500
|
+
this.eventBus.emit(parsed);
|
|
1501
|
+
}
|
|
1502
|
+
});
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
});
|
|
1506
|
+
this.client.activate();
|
|
1507
|
+
}
|
|
1508
|
+
disconnect() {
|
|
1509
|
+
if (this.client) {
|
|
1510
|
+
this.client.deactivate();
|
|
1511
|
+
this.client = null;
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
parseMessage(message, resource) {
|
|
1515
|
+
try {
|
|
1516
|
+
const raw = JSON.parse(message.body);
|
|
1517
|
+
return {
|
|
1518
|
+
action: raw.action,
|
|
1519
|
+
resource,
|
|
1520
|
+
data: raw.data,
|
|
1521
|
+
id: raw.id,
|
|
1522
|
+
ids: raw.ids,
|
|
1523
|
+
timestamp: raw.timestamp ?? Date.now(),
|
|
1524
|
+
source: "remote"
|
|
1525
|
+
};
|
|
1526
|
+
} catch {
|
|
1527
|
+
return null;
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
};
|
|
1531
|
+
|
|
530
1532
|
// src/seed.ts
|
|
531
1533
|
function seed(entityClass, data) {
|
|
532
1534
|
const entityName = entityClass.name.toLowerCase();
|
|
@@ -551,19 +1553,130 @@ function simpleHash(str) {
|
|
|
551
1553
|
// src/client.ts
|
|
552
1554
|
function createClient(config) {
|
|
553
1555
|
const driverConfig = config.driver ?? { type: "local" };
|
|
554
|
-
const
|
|
1556
|
+
const defaultDriver = createDriver(driverConfig);
|
|
555
1557
|
const client = {};
|
|
1558
|
+
const overrideDrivers = /* @__PURE__ */ new Map();
|
|
1559
|
+
if (config.overrides) {
|
|
1560
|
+
for (const [name, override] of Object.entries(config.overrides)) {
|
|
1561
|
+
overrideDrivers.set(name, createDriver(override.driver));
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
556
1564
|
for (const [name, ServiceClass] of Object.entries(config.services)) {
|
|
557
1565
|
const instance = new ServiceClass();
|
|
1566
|
+
const driver = overrideDrivers.get(name) ?? defaultDriver;
|
|
558
1567
|
instance._init(driver, name);
|
|
559
1568
|
if (driver instanceof LocalDriver) {
|
|
560
1569
|
driver.registerEntity(name, instance.entity);
|
|
561
1570
|
}
|
|
1571
|
+
if (driver instanceof HttpDriver) {
|
|
1572
|
+
driver.registerEndpoint(name, instance.endpoint);
|
|
1573
|
+
}
|
|
562
1574
|
client[name] = instance;
|
|
563
1575
|
}
|
|
564
|
-
if (config.
|
|
565
|
-
|
|
1576
|
+
if (config.auth) {
|
|
1577
|
+
const AuthClass = config.auth;
|
|
1578
|
+
const authInstance = new AuthClass();
|
|
1579
|
+
const resourceName = authInstance.entity.name.toLowerCase();
|
|
1580
|
+
if (defaultDriver instanceof LocalDriver) {
|
|
1581
|
+
authInstance._init(defaultDriver, resourceName);
|
|
1582
|
+
defaultDriver.registerEntity(resourceName, authInstance.entity);
|
|
1583
|
+
const storage = defaultDriver.getStorageBackend();
|
|
1584
|
+
authInstance._initAuth(
|
|
1585
|
+
() => {
|
|
1586
|
+
const raw = storage.getMeta("_authState");
|
|
1587
|
+
return raw ? JSON.parse(raw) : null;
|
|
1588
|
+
},
|
|
1589
|
+
(state) => {
|
|
1590
|
+
if (state) {
|
|
1591
|
+
storage.setMeta("_authState", JSON.stringify(state));
|
|
1592
|
+
} else {
|
|
1593
|
+
storage.setMeta("_authState", "");
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
);
|
|
1597
|
+
defaultDriver.setAuthProvider(() => authInstance.getAuthContext());
|
|
1598
|
+
} else if (defaultDriver instanceof HttpDriver) {
|
|
1599
|
+
authInstance._init(defaultDriver, resourceName);
|
|
1600
|
+
defaultDriver.registerEndpoint(resourceName, authInstance.endpoint);
|
|
1601
|
+
let memoryAuthState = null;
|
|
1602
|
+
authInstance._initAuth(
|
|
1603
|
+
() => memoryAuthState,
|
|
1604
|
+
(state) => {
|
|
1605
|
+
memoryAuthState = state;
|
|
1606
|
+
}
|
|
1607
|
+
);
|
|
1608
|
+
authInstance._setHttpMode(defaultDriver);
|
|
1609
|
+
defaultDriver.setAuthProvider(() => {
|
|
1610
|
+
const token = authInstance.token;
|
|
1611
|
+
return token ? { token } : null;
|
|
1612
|
+
});
|
|
1613
|
+
}
|
|
1614
|
+
client.auth = authInstance;
|
|
1615
|
+
for (const driver of overrideDrivers.values()) {
|
|
1616
|
+
if (driver instanceof HttpDriver) {
|
|
1617
|
+
driver.setAuthProvider(() => {
|
|
1618
|
+
const token = client.auth?.token;
|
|
1619
|
+
return token ? { token } : null;
|
|
1620
|
+
});
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
566
1623
|
}
|
|
1624
|
+
for (const key of Object.keys(client)) {
|
|
1625
|
+
const svc = client[key];
|
|
1626
|
+
if (svc && typeof svc._setClient === "function") {
|
|
1627
|
+
svc._setClient(client);
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
let eventSource = null;
|
|
1631
|
+
if (config.events) {
|
|
1632
|
+
const eventBus = new EventBus();
|
|
1633
|
+
client._eventBus = eventBus;
|
|
1634
|
+
for (const [name, ServiceClass] of Object.entries(config.services)) {
|
|
1635
|
+
const svc = client[name];
|
|
1636
|
+
svc._eventBus = eventBus;
|
|
1637
|
+
}
|
|
1638
|
+
const eventsConfig = config.events === true ? {} : config.events;
|
|
1639
|
+
if (eventsConfig.handlers) {
|
|
1640
|
+
for (const [resource, handler] of Object.entries(eventsConfig.handlers)) {
|
|
1641
|
+
eventBus.on(resource, handler);
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
if (eventsConfig.source) {
|
|
1645
|
+
if (eventsConfig.source.type === "sse") {
|
|
1646
|
+
eventSource = new SSESource(eventsConfig.source, eventBus);
|
|
1647
|
+
} else if (eventsConfig.source.type === "stomp") {
|
|
1648
|
+
eventSource = new STOMPSource(eventsConfig.source, eventBus);
|
|
1649
|
+
}
|
|
1650
|
+
eventSource?.connect();
|
|
1651
|
+
}
|
|
1652
|
+
client.disconnect = () => {
|
|
1653
|
+
eventSource?.disconnect();
|
|
1654
|
+
eventBus.destroy();
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
let readyPromise;
|
|
1658
|
+
if (defaultDriver instanceof LocalDriver) {
|
|
1659
|
+
if (defaultDriver.isReady) {
|
|
1660
|
+
if (config.seeds) {
|
|
1661
|
+
applySeedsIfNeeded(defaultDriver, config.seeds);
|
|
1662
|
+
}
|
|
1663
|
+
readyPromise = Promise.resolve();
|
|
1664
|
+
} else {
|
|
1665
|
+
readyPromise = defaultDriver.ready.then(() => {
|
|
1666
|
+
if (config.seeds) {
|
|
1667
|
+
applySeedsIfNeeded(defaultDriver, config.seeds);
|
|
1668
|
+
}
|
|
1669
|
+
});
|
|
1670
|
+
}
|
|
1671
|
+
} else {
|
|
1672
|
+
readyPromise = Promise.resolve();
|
|
1673
|
+
}
|
|
1674
|
+
Object.defineProperty(client, "ready", {
|
|
1675
|
+
value: readyPromise,
|
|
1676
|
+
writable: false,
|
|
1677
|
+
enumerable: false,
|
|
1678
|
+
configurable: false
|
|
1679
|
+
});
|
|
567
1680
|
return client;
|
|
568
1681
|
}
|
|
569
1682
|
function createDriver(config) {
|
|
@@ -571,7 +1684,7 @@ function createDriver(config) {
|
|
|
571
1684
|
case "local":
|
|
572
1685
|
return new LocalDriver(config);
|
|
573
1686
|
case "http":
|
|
574
|
-
|
|
1687
|
+
return new HttpDriver(config);
|
|
575
1688
|
default:
|
|
576
1689
|
throw new Error(`Unknown driver type: ${config.type}`);
|
|
577
1690
|
}
|
|
@@ -586,6 +1699,6 @@ function applySeedsIfNeeded(driver, seeds) {
|
|
|
586
1699
|
driver.setSeedVersion(newVersion);
|
|
587
1700
|
}
|
|
588
1701
|
|
|
589
|
-
export { ConflictError, Entity, FauxbaseError, ForbiddenError, NotFoundError, Service, ValidationError, afterCreate, afterUpdate, beforeCreate, beforeUpdate, computed, createClient, field, relation, seed };
|
|
1702
|
+
export { AuthService, ConflictError, Entity, EventBus, FauxbaseError, ForbiddenError, HttpDriver, HttpError, LocalDriver, NetworkError, NotFoundError, Service, TimeoutError, ValidationError, afterCreate, afterUpdate, beforeCreate, beforeUpdate, computed, createClient, defaultPreset, definePreset, djangoPreset, expressPreset, field, getPreset, laravelPreset, nestjsPreset, relation, seed, springBootPreset };
|
|
590
1703
|
//# sourceMappingURL=index.js.map
|
|
591
1704
|
//# sourceMappingURL=index.js.map
|