fauxbase 0.1.2 → 0.4.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 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,16 @@ function afterUpdate() {
159
182
  var Service = class {
160
183
  driver;
161
184
  resourceName;
185
+ client;
162
186
  /** @internal — called by createClient to wire the service */
163
187
  _init(driver, resourceName) {
164
188
  this.driver = driver;
165
189
  this.resourceName = resourceName;
166
190
  }
191
+ /** @internal — called by createClient to give services access to the client */
192
+ _setClient(client) {
193
+ this.client = client;
194
+ }
167
195
  async list(query = {}) {
168
196
  return this.driver.list(this.resourceName, query);
169
197
  }
@@ -204,6 +232,173 @@ var Service = class {
204
232
  }
205
233
  };
206
234
 
235
+ // src/auth.ts
236
+ var AuthService = class extends Service {
237
+ authState = null;
238
+ saveState = null;
239
+ httpDriver = null;
240
+ /** @internal — called by createClient to wire persistence */
241
+ _initAuth(loadState, saveState) {
242
+ this.saveState = saveState;
243
+ this.authState = loadState();
244
+ }
245
+ /** @internal — called by createClient when using HttpDriver */
246
+ _setHttpMode(driver) {
247
+ this.httpDriver = driver;
248
+ }
249
+ async login(credentials) {
250
+ if (this.httpDriver) {
251
+ return this.httpLogin(credentials);
252
+ }
253
+ return this.localLogin(credentials);
254
+ }
255
+ async register(data) {
256
+ if (this.httpDriver) {
257
+ return this.httpRegister(data);
258
+ }
259
+ return this.localRegister(data);
260
+ }
261
+ logout() {
262
+ this.authState = null;
263
+ this.persistState();
264
+ }
265
+ get currentUser() {
266
+ return this.authState ? { id: this.authState.userId, email: this.authState.email } : null;
267
+ }
268
+ get isLoggedIn() {
269
+ return this.authState !== null;
270
+ }
271
+ get token() {
272
+ return this.authState?.token ?? null;
273
+ }
274
+ hasRole(role) {
275
+ return this.authState?.role === role;
276
+ }
277
+ getAuthContext() {
278
+ if (!this.authState) return null;
279
+ return {
280
+ userId: this.authState.userId,
281
+ userName: this.authState.userName
282
+ };
283
+ }
284
+ // --- Local mode (original implementation) ---
285
+ async localLogin(credentials) {
286
+ const { items } = await this.list({ filter: { email: credentials.email } });
287
+ if (items.length === 0) {
288
+ throw new NotFoundError("Invalid email or password");
289
+ }
290
+ const user = items[0];
291
+ if (user.password !== credentials.password) {
292
+ throw new ForbiddenError("Invalid email or password");
293
+ }
294
+ this.authState = {
295
+ userId: user.id,
296
+ email: user.email,
297
+ userName: user.name || user.email,
298
+ role: user.role,
299
+ token: this.generateToken(user)
300
+ };
301
+ this.persistState();
302
+ return user;
303
+ }
304
+ async localRegister(data) {
305
+ const email = data.email;
306
+ if (email) {
307
+ const { items } = await this.list({ filter: { email } });
308
+ if (items.length > 0) {
309
+ throw new ConflictError("Email already registered");
310
+ }
311
+ }
312
+ const { data: user } = await this.create(data);
313
+ const u = user;
314
+ this.authState = {
315
+ userId: u.id,
316
+ email: u.email,
317
+ userName: u.name || u.email,
318
+ role: u.role,
319
+ token: this.generateToken(u)
320
+ };
321
+ this.persistState();
322
+ return user;
323
+ }
324
+ // --- HTTP mode ---
325
+ async httpLogin(credentials) {
326
+ const preset = this.httpDriver.preset;
327
+ const baseUrl = this.httpDriver.baseUrl;
328
+ const url = `${baseUrl}${preset.auth.loginUrl}`;
329
+ const response = await fetch(url, {
330
+ method: "POST",
331
+ headers: { "Content-Type": "application/json" },
332
+ body: JSON.stringify(credentials)
333
+ });
334
+ if (!response.ok) {
335
+ const body2 = await response.json().catch(() => ({}));
336
+ if (response.status === 401 || response.status === 403) {
337
+ throw new ForbiddenError(body2.message ?? "Invalid email or password");
338
+ }
339
+ if (response.status === 404) {
340
+ throw new NotFoundError(body2.message ?? "Invalid email or password");
341
+ }
342
+ throw new ForbiddenError(body2.message ?? "Login failed");
343
+ }
344
+ const body = await response.json();
345
+ const token = body[preset.auth.tokenField];
346
+ const user = body[preset.auth.userField] ?? body;
347
+ this.authState = {
348
+ userId: user.id,
349
+ email: user.email ?? credentials.email,
350
+ userName: user.name || user.email || credentials.email,
351
+ role: user.role,
352
+ token
353
+ };
354
+ this.persistState();
355
+ return user;
356
+ }
357
+ async httpRegister(data) {
358
+ const preset = this.httpDriver.preset;
359
+ const baseUrl = this.httpDriver.baseUrl;
360
+ const url = `${baseUrl}${preset.auth.registerUrl}`;
361
+ const response = await fetch(url, {
362
+ method: "POST",
363
+ headers: { "Content-Type": "application/json" },
364
+ body: JSON.stringify(data)
365
+ });
366
+ if (!response.ok) {
367
+ const body2 = await response.json().catch(() => ({}));
368
+ if (response.status === 409) {
369
+ throw new ConflictError(body2.message ?? "Email already registered");
370
+ }
371
+ throw new ForbiddenError(body2.message ?? "Registration 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 ?? data.email,
379
+ userName: user.name || user.email || data.email,
380
+ role: user.role,
381
+ token
382
+ };
383
+ this.persistState();
384
+ return user;
385
+ }
386
+ generateToken(user) {
387
+ return btoa(JSON.stringify({
388
+ userId: user.id,
389
+ email: user.email,
390
+ role: user.role,
391
+ iat: Date.now(),
392
+ exp: Date.now() + 24 * 60 * 60 * 1e3
393
+ }));
394
+ }
395
+ persistState() {
396
+ if (this.saveState) {
397
+ this.saveState(this.authState);
398
+ }
399
+ }
400
+ };
401
+
207
402
  // src/query-engine.ts
208
403
  var OPERATORS = [
209
404
  "startswith",
@@ -392,11 +587,143 @@ var LocalStorageBackend = class {
392
587
  localStorage.setItem(`${LS_META_PREFIX}${key}`, value);
393
588
  }
394
589
  };
590
+ function idbRequest(req) {
591
+ return new Promise((resolve, reject) => {
592
+ req.onsuccess = () => resolve(req.result);
593
+ req.onerror = () => reject(req.error);
594
+ });
595
+ }
596
+ var IDB_DATA_STORE = "data";
597
+ var IDB_META_STORE = "meta";
598
+ var IndexedDBBackend = class {
599
+ cache = new MemoryStorage();
600
+ db = null;
601
+ _ready;
602
+ constructor(dbName) {
603
+ this._ready = this.open(dbName);
604
+ }
605
+ get ready() {
606
+ return this._ready;
607
+ }
608
+ open(dbName) {
609
+ return new Promise((resolve, reject) => {
610
+ const req = indexedDB.open(dbName, 1);
611
+ req.onupgradeneeded = () => {
612
+ const db = req.result;
613
+ if (!db.objectStoreNames.contains(IDB_DATA_STORE)) {
614
+ db.createObjectStore(IDB_DATA_STORE);
615
+ }
616
+ if (!db.objectStoreNames.contains(IDB_META_STORE)) {
617
+ db.createObjectStore(IDB_META_STORE);
618
+ }
619
+ };
620
+ req.onsuccess = async () => {
621
+ this.db = req.result;
622
+ await this.loadAll();
623
+ resolve();
624
+ };
625
+ req.onerror = () => reject(req.error);
626
+ });
627
+ }
628
+ async loadAll() {
629
+ const db = this.db;
630
+ const tx = db.transaction([IDB_DATA_STORE, IDB_META_STORE], "readonly");
631
+ const dataStore = tx.objectStore(IDB_DATA_STORE);
632
+ const metaStore = tx.objectStore(IDB_META_STORE);
633
+ const dataKeys = await idbRequest(dataStore.getAllKeys());
634
+ const dataValues = await idbRequest(dataStore.getAll());
635
+ for (let i = 0; i < dataKeys.length; i++) {
636
+ const compositeKey = dataKeys[i];
637
+ const sepIdx = compositeKey.indexOf(":");
638
+ const resource = compositeKey.substring(0, sepIdx);
639
+ const id = compositeKey.substring(sepIdx + 1);
640
+ this.cache.set(resource, id, dataValues[i]);
641
+ }
642
+ const metaKeys = await idbRequest(metaStore.getAllKeys());
643
+ const metaValues = await idbRequest(metaStore.getAll());
644
+ for (let i = 0; i < metaKeys.length; i++) {
645
+ this.cache.setMeta(metaKeys[i], metaValues[i]);
646
+ }
647
+ }
648
+ writeData(resource, id, data) {
649
+ if (!this.db) return;
650
+ const tx = this.db.transaction(IDB_DATA_STORE, "readwrite");
651
+ tx.objectStore(IDB_DATA_STORE).put(data, `${resource}:${id}`);
652
+ }
653
+ deleteData(resource, id) {
654
+ if (!this.db) return;
655
+ const tx = this.db.transaction(IDB_DATA_STORE, "readwrite");
656
+ tx.objectStore(IDB_DATA_STORE).delete(`${resource}:${id}`);
657
+ }
658
+ writeMeta(key, value) {
659
+ if (!this.db) return;
660
+ const tx = this.db.transaction(IDB_META_STORE, "readwrite");
661
+ tx.objectStore(IDB_META_STORE).put(value, key);
662
+ }
663
+ getAll(resource) {
664
+ return this.cache.getAll(resource);
665
+ }
666
+ getById(resource, id) {
667
+ return this.cache.getById(resource, id);
668
+ }
669
+ set(resource, id, data) {
670
+ this.cache.set(resource, id, data);
671
+ this.writeData(resource, id, data);
672
+ }
673
+ remove(resource, id) {
674
+ this.cache.remove(resource, id);
675
+ this.deleteData(resource, id);
676
+ }
677
+ clear(resource) {
678
+ const items = this.cache.getAll(resource);
679
+ this.cache.clear(resource);
680
+ if (this.db) {
681
+ const tx = this.db.transaction(IDB_DATA_STORE, "readwrite");
682
+ const store = tx.objectStore(IDB_DATA_STORE);
683
+ for (const item of items) {
684
+ store.delete(`${resource}:${item.id}`);
685
+ }
686
+ }
687
+ }
688
+ getMeta(key) {
689
+ return this.cache.getMeta(key);
690
+ }
691
+ setMeta(key, value) {
692
+ this.cache.setMeta(key, value);
693
+ this.writeMeta(key, value);
694
+ }
695
+ };
395
696
  var LocalDriver = class {
396
697
  storage;
397
698
  entityClasses = /* @__PURE__ */ new Map();
699
+ authProvider = null;
700
+ _ready;
701
+ _isReady;
398
702
  constructor(config) {
399
- this.storage = config.persist === "localStorage" ? new LocalStorageBackend() : new MemoryStorage();
703
+ if (config.persist === "indexeddb") {
704
+ const backend = new IndexedDBBackend(config.dbName ?? "fauxbase");
705
+ this.storage = backend;
706
+ this._isReady = false;
707
+ this._ready = backend.ready.then(() => {
708
+ this._isReady = true;
709
+ });
710
+ } else {
711
+ this.storage = config.persist === "localStorage" ? new LocalStorageBackend() : new MemoryStorage();
712
+ this._isReady = true;
713
+ this._ready = Promise.resolve();
714
+ }
715
+ }
716
+ get ready() {
717
+ return this._ready;
718
+ }
719
+ get isReady() {
720
+ return this._isReady;
721
+ }
722
+ setAuthProvider(provider) {
723
+ this.authProvider = provider;
724
+ }
725
+ getStorageBackend() {
726
+ return this.storage;
400
727
  }
401
728
  registerEntity(resource, entityClass) {
402
729
  this.entityClasses.set(resource, entityClass);
@@ -419,13 +746,20 @@ var LocalDriver = class {
419
746
  async create(resource, data) {
420
747
  const entityClass = this.entityClasses.get(resource);
421
748
  const now = (/* @__PURE__ */ new Date()).toISOString();
749
+ const authContext = this.authProvider?.();
422
750
  let record = {
423
751
  ...data,
424
752
  id: data.id || generateUUID(),
425
753
  createdAt: now,
426
754
  updatedAt: now,
427
755
  deletedAt: null,
428
- version: 1
756
+ version: 1,
757
+ ...authContext ? {
758
+ createdById: authContext.userId,
759
+ createdByName: authContext.userName,
760
+ updatedById: authContext.userId,
761
+ updatedByName: authContext.userName
762
+ } : {}
429
763
  };
430
764
  if (entityClass) {
431
765
  record = applyDefaults(record, entityClass);
@@ -444,13 +778,18 @@ var LocalDriver = class {
444
778
  if (entityClass) {
445
779
  validateEntity(data, entityClass, false);
446
780
  }
781
+ const authContext = this.authProvider?.();
447
782
  const record = {
448
783
  ...existing,
449
784
  ...data,
450
785
  id,
451
786
  createdAt: existing.createdAt,
452
787
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
453
- version: (existing.version || 0) + 1
788
+ version: (existing.version || 0) + 1,
789
+ ...authContext ? {
790
+ updatedById: authContext.userId,
791
+ updatedByName: authContext.userName
792
+ } : {}
454
793
  };
455
794
  this.storage.set(resource, id, record);
456
795
  const result = entityClass ? applyComputedFields(record, entityClass) : record;
@@ -462,11 +801,18 @@ var LocalDriver = class {
462
801
  throw new NotFoundError(`${resource} with id "${id}" not found`);
463
802
  }
464
803
  const now = (/* @__PURE__ */ new Date()).toISOString();
804
+ const authContext = this.authProvider?.();
465
805
  const record = {
466
806
  ...existing,
467
807
  deletedAt: now,
468
808
  updatedAt: now,
469
- version: (existing.version || 0) + 1
809
+ version: (existing.version || 0) + 1,
810
+ ...authContext ? {
811
+ deletedById: authContext.userId,
812
+ deletedByName: authContext.userName,
813
+ updatedById: authContext.userId,
814
+ updatedByName: authContext.userName
815
+ } : {}
470
816
  };
471
817
  this.storage.set(resource, id, record);
472
818
  return { data: record };
@@ -529,6 +875,491 @@ var LocalDriver = class {
529
875
  }
530
876
  };
531
877
 
878
+ // src/presets/types.ts
879
+ function definePreset(config) {
880
+ return config;
881
+ }
882
+
883
+ // src/presets/default.ts
884
+ var defaultPreset = definePreset({
885
+ name: "default",
886
+ response: {
887
+ single: (raw) => ({ data: raw.data ?? raw }),
888
+ list: (raw) => ({
889
+ items: raw.items ?? raw.data ?? [],
890
+ meta: raw.meta ?? {}
891
+ }),
892
+ error: (raw) => ({
893
+ error: raw.error ?? raw.message ?? "Unknown error",
894
+ code: raw.code ?? "UNKNOWN",
895
+ details: raw.details
896
+ })
897
+ },
898
+ meta: { page: "page", size: "size", totalItems: "totalItems", totalPages: "totalPages" },
899
+ query: {
900
+ filterStyle: "django",
901
+ pageParam: "page",
902
+ sizeParam: "size",
903
+ sortParam: "sort",
904
+ sortFormat: "field,direction"
905
+ },
906
+ auth: {
907
+ loginUrl: "/auth/login",
908
+ registerUrl: "/auth/register",
909
+ logoutUrl: "/auth/logout",
910
+ tokenField: "token",
911
+ userField: "user",
912
+ headerFormat: "Bearer {token}"
913
+ }
914
+ });
915
+
916
+ // src/presets/spring-boot.ts
917
+ var springBootPreset = definePreset({
918
+ name: "spring-boot",
919
+ response: {
920
+ single: (raw) => ({ data: raw }),
921
+ list: (raw) => ({
922
+ items: raw.content ?? [],
923
+ meta: {
924
+ page: (raw.pageable?.pageNumber ?? 0) + 1,
925
+ size: raw.pageable?.pageSize ?? raw.size ?? 20,
926
+ totalItems: raw.totalElements ?? 0,
927
+ totalPages: raw.totalPages ?? 0
928
+ }
929
+ }),
930
+ error: (raw) => ({
931
+ error: raw.message ?? raw.error ?? "Unknown error",
932
+ code: raw.status?.toString() ?? "UNKNOWN",
933
+ details: raw.errors?.reduce?.((acc, e) => {
934
+ acc[e.field ?? e.code ?? "unknown"] = e.defaultMessage ?? e.message ?? "";
935
+ return acc;
936
+ }, {}) ?? void 0
937
+ })
938
+ },
939
+ meta: { page: "page", size: "size", totalItems: "totalElements", totalPages: "totalPages" },
940
+ query: {
941
+ filterStyle: "dot",
942
+ pageParam: "page",
943
+ sizeParam: "size",
944
+ sortParam: "sort",
945
+ sortFormat: "field,direction",
946
+ pageOffset: -1
947
+ // 0-indexed
948
+ },
949
+ auth: {
950
+ loginUrl: "/api/auth/login",
951
+ registerUrl: "/api/auth/register",
952
+ tokenField: "token",
953
+ userField: "user",
954
+ headerFormat: "Bearer {token}"
955
+ }
956
+ });
957
+
958
+ // src/presets/laravel.ts
959
+ var laravelPreset = definePreset({
960
+ name: "laravel",
961
+ response: {
962
+ single: (raw) => ({ data: raw.data ?? raw }),
963
+ list: (raw) => ({
964
+ items: raw.data ?? [],
965
+ meta: {
966
+ page: raw.meta?.current_page ?? raw.current_page ?? 1,
967
+ size: raw.meta?.per_page ?? raw.per_page ?? 15,
968
+ totalItems: raw.meta?.total ?? raw.total ?? 0,
969
+ totalPages: raw.meta?.last_page ?? raw.last_page ?? 0
970
+ }
971
+ }),
972
+ error: (raw) => ({
973
+ error: raw.message ?? "Unknown error",
974
+ code: raw.status?.toString() ?? "UNKNOWN",
975
+ details: raw.errors ? Object.entries(raw.errors).reduce((acc, [key, val]) => {
976
+ acc[key] = Array.isArray(val) ? val[0] : val;
977
+ return acc;
978
+ }, {}) : void 0
979
+ })
980
+ },
981
+ meta: { page: "current_page", size: "per_page", totalItems: "total", totalPages: "last_page" },
982
+ query: {
983
+ filterStyle: "bracket",
984
+ pageParam: "page",
985
+ sizeParam: "per_page",
986
+ sortParam: "sort",
987
+ sortFormat: "field,direction"
988
+ },
989
+ auth: {
990
+ loginUrl: "/api/login",
991
+ registerUrl: "/api/register",
992
+ logoutUrl: "/api/logout",
993
+ tokenField: "token",
994
+ userField: "user",
995
+ headerFormat: "Bearer {token}"
996
+ }
997
+ });
998
+
999
+ // src/presets/django.ts
1000
+ var djangoPreset = definePreset({
1001
+ name: "django",
1002
+ response: {
1003
+ single: (raw) => ({ data: raw }),
1004
+ list: (raw) => ({
1005
+ items: raw.results ?? [],
1006
+ meta: {
1007
+ page: 1,
1008
+ // Django REST doesn't always include page number
1009
+ size: raw.results?.length ?? 0,
1010
+ totalItems: raw.count ?? 0,
1011
+ totalPages: raw.count && raw.results?.length ? Math.ceil(raw.count / raw.results.length) : 0
1012
+ }
1013
+ }),
1014
+ error: (raw) => ({
1015
+ error: raw.detail ?? raw.message ?? "Unknown error",
1016
+ code: raw.status_code?.toString() ?? "UNKNOWN",
1017
+ details: typeof raw === "object" && !raw.detail ? Object.entries(raw).reduce((acc, [key, val]) => {
1018
+ if (key !== "status_code") {
1019
+ acc[key] = Array.isArray(val) ? val[0] : String(val);
1020
+ }
1021
+ return acc;
1022
+ }, {}) : void 0
1023
+ })
1024
+ },
1025
+ meta: { page: "page", size: "page_size", totalItems: "count", totalPages: "total_pages" },
1026
+ query: {
1027
+ filterStyle: "django",
1028
+ pageParam: "page",
1029
+ sizeParam: "page_size",
1030
+ sortParam: "ordering",
1031
+ sortFormat: "field,direction"
1032
+ },
1033
+ auth: {
1034
+ loginUrl: "/api/auth/login/",
1035
+ registerUrl: "/api/auth/register/",
1036
+ logoutUrl: "/api/auth/logout/",
1037
+ tokenField: "token",
1038
+ userField: "user",
1039
+ headerFormat: "Token {token}"
1040
+ }
1041
+ });
1042
+
1043
+ // src/presets/nestjs.ts
1044
+ var nestjsPreset = definePreset({
1045
+ name: "nestjs",
1046
+ response: {
1047
+ single: (raw) => ({ data: raw.data ?? raw }),
1048
+ list: (raw) => ({
1049
+ items: raw.data ?? raw.items ?? [],
1050
+ meta: raw.meta ?? {}
1051
+ }),
1052
+ error: (raw) => ({
1053
+ error: raw.message ?? "Unknown error",
1054
+ code: raw.error ?? raw.statusCode?.toString() ?? "UNKNOWN",
1055
+ details: raw.message && Array.isArray(raw.message) ? raw.message.reduce((acc, msg, i) => {
1056
+ acc[`field_${i}`] = msg;
1057
+ return acc;
1058
+ }, {}) : void 0
1059
+ })
1060
+ },
1061
+ meta: { page: "page", size: "limit", totalItems: "totalItems", totalPages: "totalPages" },
1062
+ query: {
1063
+ filterStyle: "nestjs",
1064
+ pageParam: "page",
1065
+ sizeParam: "limit",
1066
+ sortParam: "sort",
1067
+ sortFormat: "field:direction"
1068
+ },
1069
+ auth: {
1070
+ loginUrl: "/auth/login",
1071
+ registerUrl: "/auth/register",
1072
+ tokenField: "access_token",
1073
+ userField: "user",
1074
+ headerFormat: "Bearer {token}"
1075
+ }
1076
+ });
1077
+
1078
+ // src/presets/express.ts
1079
+ var expressPreset = definePreset({
1080
+ name: "express",
1081
+ response: {
1082
+ single: (raw) => ({ data: raw.data ?? raw }),
1083
+ list: (raw) => ({
1084
+ items: raw.data ?? raw.items ?? [],
1085
+ meta: raw.meta ?? {}
1086
+ }),
1087
+ error: (raw) => ({
1088
+ error: raw.error ?? raw.message ?? "Unknown error",
1089
+ code: raw.code ?? "UNKNOWN",
1090
+ details: raw.details
1091
+ })
1092
+ },
1093
+ meta: { page: "page", size: "size", totalItems: "totalItems", totalPages: "totalPages" },
1094
+ query: {
1095
+ filterStyle: "django",
1096
+ pageParam: "page",
1097
+ sizeParam: "size",
1098
+ sortParam: "sort",
1099
+ sortFormat: "field,direction"
1100
+ },
1101
+ auth: {
1102
+ loginUrl: "/api/auth/login",
1103
+ registerUrl: "/api/auth/register",
1104
+ logoutUrl: "/api/auth/logout",
1105
+ tokenField: "token",
1106
+ userField: "user",
1107
+ headerFormat: "Bearer {token}"
1108
+ }
1109
+ });
1110
+
1111
+ // src/presets/index.ts
1112
+ var presetRegistry = /* @__PURE__ */ new Map([
1113
+ ["default", defaultPreset],
1114
+ ["spring-boot", springBootPreset],
1115
+ ["laravel", laravelPreset],
1116
+ ["django", djangoPreset],
1117
+ ["nestjs", nestjsPreset],
1118
+ ["express", expressPreset]
1119
+ ]);
1120
+ function getPreset(name) {
1121
+ const preset = presetRegistry.get(name);
1122
+ if (!preset) {
1123
+ throw new Error(`Unknown preset: "${name}". Available: ${Array.from(presetRegistry.keys()).join(", ")}`);
1124
+ }
1125
+ return preset;
1126
+ }
1127
+
1128
+ // src/drivers/query-serializer.ts
1129
+ function serializeQuery(query, config) {
1130
+ const params = new URLSearchParams();
1131
+ if (query.filter) {
1132
+ serializeFilters(params, query.filter, config.filterStyle);
1133
+ }
1134
+ if (query.sort && config.sortParam) {
1135
+ const direction = query.sort.direction;
1136
+ if (config.sortParam === "ordering") {
1137
+ params.set(config.sortParam, direction === "desc" ? `-${query.sort.field}` : query.sort.field);
1138
+ } else if (config.sortFormat === "field:direction") {
1139
+ params.set(config.sortParam, `${query.sort.field}:${direction}`);
1140
+ } else {
1141
+ params.set(config.sortParam, `${query.sort.field},${direction}`);
1142
+ }
1143
+ }
1144
+ if (query.page !== void 0) {
1145
+ const pageOffset = config.pageOffset ?? 0;
1146
+ params.set(config.pageParam, String(query.page + pageOffset));
1147
+ }
1148
+ if (query.size !== void 0) {
1149
+ params.set(config.sizeParam, String(query.size));
1150
+ }
1151
+ return params;
1152
+ }
1153
+ function serializeFilters(params, filter, style) {
1154
+ for (const [key, value] of Object.entries(filter)) {
1155
+ if (value === void 0) continue;
1156
+ const serialized = typeof value === "object" && !Array.isArray(value) ? JSON.stringify(value) : String(value);
1157
+ switch (style) {
1158
+ case "django":
1159
+ params.set(key, serialized);
1160
+ break;
1161
+ case "dot":
1162
+ params.set(key.replace(/__/g, "."), serialized);
1163
+ break;
1164
+ case "bracket": {
1165
+ const bracketKey = key.replace(/__/g, "_");
1166
+ params.set(`filter[${bracketKey}]`, serialized);
1167
+ break;
1168
+ }
1169
+ case "nestjs": {
1170
+ const parts = key.split("__");
1171
+ if (parts.length === 2) {
1172
+ params.set(`filter.${parts[0]}.$${parts[1]}`, serialized);
1173
+ } else {
1174
+ params.set(`filter.${key}`, serialized);
1175
+ }
1176
+ break;
1177
+ }
1178
+ }
1179
+ }
1180
+ }
1181
+
1182
+ // src/drivers/http.ts
1183
+ var HttpDriver = class {
1184
+ baseUrl;
1185
+ preset;
1186
+ timeout;
1187
+ maxRetries;
1188
+ baseDelay;
1189
+ defaultHeaders;
1190
+ endpoints = /* @__PURE__ */ new Map();
1191
+ authProvider = null;
1192
+ constructor(config) {
1193
+ this.baseUrl = config.baseUrl.replace(/\/$/, "");
1194
+ this.preset = typeof config.preset === "string" ? getPreset(config.preset ?? "default") : config.preset ?? getPreset("default");
1195
+ this.timeout = config.timeout ?? 3e4;
1196
+ this.maxRetries = config.retry?.maxRetries ?? 3;
1197
+ this.baseDelay = config.retry?.baseDelay ?? 300;
1198
+ this.defaultHeaders = config.headers ?? {};
1199
+ }
1200
+ setAuthProvider(provider) {
1201
+ this.authProvider = provider;
1202
+ }
1203
+ registerEndpoint(resource, endpoint) {
1204
+ this.endpoints.set(resource, endpoint);
1205
+ }
1206
+ getEndpoint(resource) {
1207
+ return this.endpoints.get(resource) ?? `/${resource}`;
1208
+ }
1209
+ buildUrl(resource, id) {
1210
+ const endpoint = this.getEndpoint(resource);
1211
+ const base = `${this.baseUrl}${endpoint}`;
1212
+ return id ? `${base}/${id}` : base;
1213
+ }
1214
+ buildHeaders() {
1215
+ const headers = {
1216
+ "Content-Type": "application/json",
1217
+ ...this.defaultHeaders
1218
+ };
1219
+ const auth = this.authProvider?.();
1220
+ if (auth?.token) {
1221
+ headers["Authorization"] = this.preset.auth.headerFormat.replace("{token}", auth.token);
1222
+ }
1223
+ return headers;
1224
+ }
1225
+ async request(url, options = {}, retryCount = 0) {
1226
+ const controller = new AbortController();
1227
+ const timer = setTimeout(() => controller.abort(), this.timeout);
1228
+ try {
1229
+ const response = await fetch(url, {
1230
+ ...options,
1231
+ headers: { ...this.buildHeaders(), ...options.headers ?? {} },
1232
+ signal: controller.signal
1233
+ });
1234
+ clearTimeout(timer);
1235
+ if (!response.ok) {
1236
+ if (response.status >= 500 && retryCount < this.maxRetries) {
1237
+ const delay = this.baseDelay * Math.pow(2, retryCount);
1238
+ await new Promise((r) => setTimeout(r, delay));
1239
+ return this.request(url, options, retryCount + 1);
1240
+ }
1241
+ const body = await response.json().catch(() => ({}));
1242
+ this.throwMappedError(response.status, body);
1243
+ }
1244
+ if (response.status === 204) {
1245
+ return {};
1246
+ }
1247
+ return response.json();
1248
+ } catch (err) {
1249
+ clearTimeout(timer);
1250
+ if (err instanceof FauxbaseError) throw err;
1251
+ if (err.name === "AbortError") {
1252
+ throw new TimeoutError(`Request timed out after ${this.timeout}ms`);
1253
+ }
1254
+ throw new NetworkError(err.message ?? "Network request failed");
1255
+ }
1256
+ }
1257
+ throwMappedError(status, body) {
1258
+ const parsed = this.preset.response.error(body);
1259
+ switch (true) {
1260
+ case (status === 400 || status === 422):
1261
+ throw new ValidationError(parsed.error, parsed.details);
1262
+ case (status === 401 || status === 403):
1263
+ throw new ForbiddenError(parsed.error);
1264
+ case status === 404:
1265
+ throw new NotFoundError(parsed.error);
1266
+ case status === 409:
1267
+ throw new ConflictError(parsed.error);
1268
+ default:
1269
+ throw new HttpError(parsed.error, status, parsed.details);
1270
+ }
1271
+ }
1272
+ async list(resource, query) {
1273
+ const url = this.buildUrl(resource);
1274
+ const params = serializeQuery(query, this.preset.query);
1275
+ const queryString = params.toString();
1276
+ const fullUrl = queryString ? `${url}?${queryString}` : url;
1277
+ const raw = await this.request(fullUrl);
1278
+ const parsed = this.preset.response.list(raw);
1279
+ return {
1280
+ items: parsed.items,
1281
+ meta: {
1282
+ page: parsed.meta[this.preset.meta.page] ?? parsed.meta.page ?? query.page ?? 1,
1283
+ size: parsed.meta[this.preset.meta.size] ?? parsed.meta.size ?? query.size ?? 20,
1284
+ totalItems: parsed.meta[this.preset.meta.totalItems] ?? parsed.meta.totalItems ?? 0,
1285
+ totalPages: parsed.meta[this.preset.meta.totalPages] ?? parsed.meta.totalPages ?? 0
1286
+ }
1287
+ };
1288
+ }
1289
+ async get(resource, id) {
1290
+ const url = this.buildUrl(resource, id);
1291
+ const raw = await this.request(url);
1292
+ return this.preset.response.single(raw);
1293
+ }
1294
+ async create(resource, data) {
1295
+ const url = this.buildUrl(resource);
1296
+ const raw = await this.request(url, {
1297
+ method: "POST",
1298
+ body: JSON.stringify(data)
1299
+ });
1300
+ return this.preset.response.single(raw);
1301
+ }
1302
+ async update(resource, id, data) {
1303
+ const url = this.buildUrl(resource, id);
1304
+ const raw = await this.request(url, {
1305
+ method: "PATCH",
1306
+ body: JSON.stringify(data)
1307
+ });
1308
+ return this.preset.response.single(raw);
1309
+ }
1310
+ async delete(resource, id) {
1311
+ const url = this.buildUrl(resource, id);
1312
+ const raw = await this.request(url, {
1313
+ method: "DELETE"
1314
+ });
1315
+ return this.preset.response.single(raw);
1316
+ }
1317
+ async count(resource, filter) {
1318
+ const url = `${this.buildUrl(resource)}/count`;
1319
+ const params = filter ? serializeQuery({ filter }, this.preset.query) : new URLSearchParams();
1320
+ const queryString = params.toString();
1321
+ const fullUrl = queryString ? `${url}?${queryString}` : url;
1322
+ const raw = await this.request(fullUrl);
1323
+ return raw.count ?? raw.data?.count ?? 0;
1324
+ }
1325
+ async bulkCreate(resource, data) {
1326
+ const url = `${this.buildUrl(resource)}/bulk`;
1327
+ const raw = await this.request(url, {
1328
+ method: "POST",
1329
+ body: JSON.stringify(data)
1330
+ });
1331
+ const parsed = this.preset.response.single(raw);
1332
+ return { data: Array.isArray(parsed.data) ? parsed.data : [parsed.data] };
1333
+ }
1334
+ async bulkUpdate(resource, updates) {
1335
+ const url = `${this.buildUrl(resource)}/bulk`;
1336
+ const raw = await this.request(url, {
1337
+ method: "PATCH",
1338
+ body: JSON.stringify(updates)
1339
+ });
1340
+ const parsed = this.preset.response.single(raw);
1341
+ return { data: Array.isArray(parsed.data) ? parsed.data : [parsed.data] };
1342
+ }
1343
+ async bulkDelete(resource, ids) {
1344
+ const url = `${this.buildUrl(resource)}/bulk`;
1345
+ const raw = await this.request(url, {
1346
+ method: "DELETE",
1347
+ body: JSON.stringify({ ids })
1348
+ });
1349
+ return { data: { count: raw.count ?? raw.data?.count ?? ids.length } };
1350
+ }
1351
+ // Seed methods are no-ops for HTTP — backend owns data
1352
+ seed() {
1353
+ }
1354
+ getSeedVersion() {
1355
+ return null;
1356
+ }
1357
+ setSeedVersion() {
1358
+ }
1359
+ clear() {
1360
+ }
1361
+ };
1362
+
532
1363
  // src/seed.ts
533
1364
  function seed(entityClass, data) {
534
1365
  const entityName = entityClass.name.toLowerCase();
@@ -553,19 +1384,103 @@ function simpleHash(str) {
553
1384
  // src/client.ts
554
1385
  function createClient(config) {
555
1386
  const driverConfig = config.driver ?? { type: "local" };
556
- const driver = createDriver(driverConfig);
1387
+ const defaultDriver = createDriver(driverConfig);
557
1388
  const client = {};
1389
+ const overrideDrivers = /* @__PURE__ */ new Map();
1390
+ if (config.overrides) {
1391
+ for (const [name, override] of Object.entries(config.overrides)) {
1392
+ overrideDrivers.set(name, createDriver(override.driver));
1393
+ }
1394
+ }
558
1395
  for (const [name, ServiceClass] of Object.entries(config.services)) {
559
1396
  const instance = new ServiceClass();
1397
+ const driver = overrideDrivers.get(name) ?? defaultDriver;
560
1398
  instance._init(driver, name);
561
1399
  if (driver instanceof LocalDriver) {
562
1400
  driver.registerEntity(name, instance.entity);
563
1401
  }
1402
+ if (driver instanceof HttpDriver) {
1403
+ driver.registerEndpoint(name, instance.endpoint);
1404
+ }
564
1405
  client[name] = instance;
565
1406
  }
566
- if (config.seeds && driver instanceof LocalDriver) {
567
- applySeedsIfNeeded(driver, config.seeds);
1407
+ if (config.auth) {
1408
+ const AuthClass = config.auth;
1409
+ const authInstance = new AuthClass();
1410
+ const resourceName = authInstance.entity.name.toLowerCase();
1411
+ if (defaultDriver instanceof LocalDriver) {
1412
+ authInstance._init(defaultDriver, resourceName);
1413
+ defaultDriver.registerEntity(resourceName, authInstance.entity);
1414
+ const storage = defaultDriver.getStorageBackend();
1415
+ authInstance._initAuth(
1416
+ () => {
1417
+ const raw = storage.getMeta("_authState");
1418
+ return raw ? JSON.parse(raw) : null;
1419
+ },
1420
+ (state) => {
1421
+ if (state) {
1422
+ storage.setMeta("_authState", JSON.stringify(state));
1423
+ } else {
1424
+ storage.setMeta("_authState", "");
1425
+ }
1426
+ }
1427
+ );
1428
+ defaultDriver.setAuthProvider(() => authInstance.getAuthContext());
1429
+ } else if (defaultDriver instanceof HttpDriver) {
1430
+ authInstance._init(defaultDriver, resourceName);
1431
+ defaultDriver.registerEndpoint(resourceName, authInstance.endpoint);
1432
+ let memoryAuthState = null;
1433
+ authInstance._initAuth(
1434
+ () => memoryAuthState,
1435
+ (state) => {
1436
+ memoryAuthState = state;
1437
+ }
1438
+ );
1439
+ authInstance._setHttpMode(defaultDriver);
1440
+ defaultDriver.setAuthProvider(() => {
1441
+ const token = authInstance.token;
1442
+ return token ? { token } : null;
1443
+ });
1444
+ }
1445
+ client.auth = authInstance;
1446
+ for (const driver of overrideDrivers.values()) {
1447
+ if (driver instanceof HttpDriver) {
1448
+ driver.setAuthProvider(() => {
1449
+ const token = client.auth?.token;
1450
+ return token ? { token } : null;
1451
+ });
1452
+ }
1453
+ }
568
1454
  }
1455
+ for (const key of Object.keys(client)) {
1456
+ const svc = client[key];
1457
+ if (svc && typeof svc._setClient === "function") {
1458
+ svc._setClient(client);
1459
+ }
1460
+ }
1461
+ let readyPromise;
1462
+ if (defaultDriver instanceof LocalDriver) {
1463
+ if (defaultDriver.isReady) {
1464
+ if (config.seeds) {
1465
+ applySeedsIfNeeded(defaultDriver, config.seeds);
1466
+ }
1467
+ readyPromise = Promise.resolve();
1468
+ } else {
1469
+ readyPromise = defaultDriver.ready.then(() => {
1470
+ if (config.seeds) {
1471
+ applySeedsIfNeeded(defaultDriver, config.seeds);
1472
+ }
1473
+ });
1474
+ }
1475
+ } else {
1476
+ readyPromise = Promise.resolve();
1477
+ }
1478
+ Object.defineProperty(client, "ready", {
1479
+ value: readyPromise,
1480
+ writable: false,
1481
+ enumerable: false,
1482
+ configurable: false
1483
+ });
569
1484
  return client;
570
1485
  }
571
1486
  function createDriver(config) {
@@ -573,7 +1488,7 @@ function createDriver(config) {
573
1488
  case "local":
574
1489
  return new LocalDriver(config);
575
1490
  case "http":
576
- throw new Error("HttpDriver not implemented yet");
1491
+ return new HttpDriver(config);
577
1492
  default:
578
1493
  throw new Error(`Unknown driver type: ${config.type}`);
579
1494
  }
@@ -588,12 +1503,18 @@ function applySeedsIfNeeded(driver, seeds) {
588
1503
  driver.setSeedVersion(newVersion);
589
1504
  }
590
1505
 
1506
+ exports.AuthService = AuthService;
591
1507
  exports.ConflictError = ConflictError;
592
1508
  exports.Entity = Entity;
593
1509
  exports.FauxbaseError = FauxbaseError;
594
1510
  exports.ForbiddenError = ForbiddenError;
1511
+ exports.HttpDriver = HttpDriver;
1512
+ exports.HttpError = HttpError;
1513
+ exports.LocalDriver = LocalDriver;
1514
+ exports.NetworkError = NetworkError;
595
1515
  exports.NotFoundError = NotFoundError;
596
1516
  exports.Service = Service;
1517
+ exports.TimeoutError = TimeoutError;
597
1518
  exports.ValidationError = ValidationError;
598
1519
  exports.afterCreate = afterCreate;
599
1520
  exports.afterUpdate = afterUpdate;
@@ -601,8 +1522,16 @@ exports.beforeCreate = beforeCreate;
601
1522
  exports.beforeUpdate = beforeUpdate;
602
1523
  exports.computed = computed;
603
1524
  exports.createClient = createClient;
1525
+ exports.defaultPreset = defaultPreset;
1526
+ exports.definePreset = definePreset;
1527
+ exports.djangoPreset = djangoPreset;
1528
+ exports.expressPreset = expressPreset;
604
1529
  exports.field = field;
1530
+ exports.getPreset = getPreset;
1531
+ exports.laravelPreset = laravelPreset;
1532
+ exports.nestjsPreset = nestjsPreset;
605
1533
  exports.relation = relation;
606
1534
  exports.seed = seed;
1535
+ exports.springBootPreset = springBootPreset;
607
1536
  //# sourceMappingURL=index.cjs.map
608
1537
  //# sourceMappingURL=index.cjs.map