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