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