@delmaredigital/payload-better-auth 0.4.3 → 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.
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * @packageDocumentation
8
8
  */
9
- import type { Adapter, BetterAuthOptions } from 'better-auth';
9
+ import type { DBAdapter, BetterAuthOptions } from 'better-auth';
10
10
  import type { BasePayload } from 'payload';
11
11
  /**
12
12
  * Database types supported by Payload CMS.
@@ -48,18 +48,6 @@ export type PayloadAdapterConfig = {
48
48
  * - 'text' for UUID
49
49
  */
50
50
  idType?: 'number' | 'text';
51
- /**
52
- * Additional fields to convert to numeric IDs beyond the *Id heuristic.
53
- * Use when you have ID fields that don't follow the naming convention.
54
- * @example ['customOrgRef', 'legacyIdentifier']
55
- */
56
- idFieldsAllowlist?: string[];
57
- /**
58
- * Fields to exclude from numeric ID conversion.
59
- * Use when a field ends in 'Id' but isn't actually an ID reference.
60
- * @example ['visitorId', 'correlationId']
61
- */
62
- idFieldsBlocklist?: string[];
63
51
  };
64
52
  };
65
53
  /**
@@ -96,5 +84,5 @@ export type PayloadAdapterConfig = {
96
84
  * })
97
85
  * ```
98
86
  */
99
- export declare function payloadAdapter({ payloadClient, adapterConfig, }: PayloadAdapterConfig): (options: BetterAuthOptions) => Adapter;
100
- export type { Adapter, BetterAuthOptions };
87
+ export declare function payloadAdapter({ payloadClient, adapterConfig, }: PayloadAdapterConfig): (options: BetterAuthOptions) => DBAdapter;
88
+ export type { DBAdapter, BetterAuthOptions };
@@ -63,9 +63,7 @@
63
63
  * })
64
64
  * ```
65
65
  */ export function payloadAdapter({ payloadClient, adapterConfig = {} }) {
66
- const { enableDebugLogs = false, idFieldsAllowlist = [], idFieldsBlocklist = [] } = adapterConfig;
67
- const idFieldsAllowlistSet = new Set(idFieldsAllowlist);
68
- const idFieldsBlocklistSet = new Set(idFieldsBlocklist);
66
+ const { enableDebugLogs = false } = adapterConfig;
69
67
  // Resolve payload client (supports lazy initialization)
70
68
  async function resolvePayloadClient() {
71
69
  return typeof payloadClient === 'function' ? await payloadClient() : payloadClient;
@@ -180,7 +178,9 @@
180
178
  disableIdGeneration: true,
181
179
  // MongoDB uses ObjectId strings, not numeric IDs
182
180
  supportsNumericIds: effectiveDbType !== 'mongodb',
183
- supportsDates: true,
181
+ // Payload returns dates as ISO strings via its Local API, not Date objects.
182
+ // Setting false tells the factory to convert string dates ↔ Date objects.
183
+ supportsDates: false,
184
184
  supportsBooleans: true,
185
185
  supportsJSON: true,
186
186
  supportsArrays: false,
@@ -211,22 +211,21 @@
211
211
  }
212
212
  return resolvePromise;
213
213
  };
214
- // Helper to convert ID based on type
215
- const convertId = (id)=>{
216
- if (idType === 'number' && typeof id === 'string') {
217
- const num = parseInt(id, 10);
218
- return isNaN(num) ? id : num;
219
- }
220
- if (idType === 'text' && typeof id === 'number') {
221
- return String(id);
222
- }
223
- return id;
224
- };
225
214
  // Create the adapter using createAdapterFactory
226
215
  // The factory handles all schema-aware transformations for us
227
216
  const adapterFactory = createAdapterFactory({
228
217
  config: factoryConfig,
229
- adapter: ({ schema, getModelName, getFieldName, debugLog })=>{
218
+ adapter: ({ schema, getModelName, debugLog })=>{
219
+ // Set fieldName on reference fields so the factory maps userId→user, etc.
220
+ // Payload uses relationship fields without the Id suffix.
221
+ for (const table of Object.values(schema)){
222
+ for (const [fieldKey, fieldDef] of Object.entries(table.fields)){
223
+ if (fieldDef.references) {
224
+ const stripped = fieldKey.replace(/(_id|Id)$/, '');
225
+ if (stripped !== fieldKey) fieldDef.fieldName = stripped;
226
+ }
227
+ }
228
+ }
230
229
  // Log initialization
231
230
  if (enableDebugLogs) {
232
231
  debugLog('Adapter initialized', {
@@ -235,126 +234,15 @@
235
234
  });
236
235
  }
237
236
  /**
238
- * Get the schema for a model, handling plural/singular lookups.
239
- * Better Auth queries with plural names when usePlural is true,
240
- * but schema keys are singular.
241
- */ function getModelSchema(model) {
242
- // First try direct lookup
243
- if (schema[model]) return schema[model];
244
- // Try singular form (strip trailing 's') for plural model names
245
- const singular = model.endsWith('s') ? model.slice(0, -1) : model;
246
- if (schema[singular]) return schema[singular];
247
- // Try without 'ies' → 'y' conversion (e.g., 'verifications' → 'verification')
248
- // This handles edge cases but 'verifications' → 'verification' works with simple 's' strip
249
- return undefined;
250
- }
251
- /**
252
- * Transform a Better Auth field name to a Payload field name.
253
- *
254
- * For reference fields (those with `references` in schema), Payload collections
255
- * use the field name without the `Id`/`_id` suffix (e.g., `userId` → `user`).
256
- * This matches how betterAuthCollections() generates relationship fields.
257
- */ function getPayloadFieldName(model, field) {
258
- // First apply any custom field name mappings from BetterAuthOptions
259
- const mappedField = getFieldName({
260
- model,
261
- field
262
- });
263
- // Check if this field is a reference field in the schema
264
- const modelSchema = getModelSchema(model);
265
- if (modelSchema?.fields?.[field]?.references) {
266
- // Strip _id or Id suffix for reference fields
267
- // This matches betterAuthCollections() which does: fieldName.replace(/(_id|Id)$/, '')
268
- return mappedField.replace(/(_id|Id)$/, '');
269
- }
270
- return mappedField;
271
- }
272
- /**
273
- * Transform input data from Better Auth format to Payload format.
274
- * Converts reference field names (e.g., `userId` → `user`) and
275
- * converts reference field values to the correct ID type.
276
- */ function transformDataForPayload(model, data) {
277
- const modelSchema = getModelSchema(model);
278
- if (!modelSchema?.fields) return data;
279
- const transformed = {};
280
- for (const [key, value] of Object.entries(data)){
281
- const payloadKey = getPayloadFieldName(model, key);
282
- let transformedValue = value;
283
- // If this is a reference field, convert the ID to the correct type
284
- const fieldDef = modelSchema.fields[key];
285
- if (fieldDef?.references && value !== null && value !== undefined) {
286
- // Convert reference ID to the correct type (number for SERIAL, string for UUID)
287
- if (idType === 'number' && typeof value === 'string') {
288
- const numValue = parseInt(value, 10);
289
- if (!isNaN(numValue)) {
290
- transformedValue = numValue;
291
- }
292
- } else if (idType === 'text' && typeof value === 'number') {
293
- transformedValue = String(value);
294
- }
295
- }
296
- transformed[payloadKey] = transformedValue;
297
- }
298
- if (enableDebugLogs) {
299
- console.log('[payload-adapter] transformDataForPayload:', {
300
- model,
301
- inputKeys: Object.keys(data),
302
- outputKeys: Object.keys(transformed),
303
- transformedData: transformed
304
- });
305
- }
306
- return transformed;
307
- }
308
- /**
309
- * Transform output data from Payload format to Better Auth format.
310
- * Converts reference field names back (e.g., `user` → `userId`).
311
- */ function transformDataFromPayload(model, data) {
312
- const modelSchema = getModelSchema(model);
313
- if (!modelSchema?.fields || !data) return data;
314
- const transformed = {
315
- ...data
316
- };
317
- // For each field in the schema that has references,
318
- // check if Payload returned the stripped name and map it back
319
- for (const [fieldKey, fieldDef] of Object.entries(modelSchema.fields)){
320
- if (fieldDef.references) {
321
- const payloadFieldName = fieldKey.replace(/(_id|Id)$/, '');
322
- if (payloadFieldName in data && !(fieldKey in transformed)) {
323
- transformed[fieldKey] = data[payloadFieldName];
324
- // Keep both for compatibility - Better Auth expects userId
325
- }
326
- }
327
- }
328
- // Convert semantic ID fields to numbers when using serial IDs
329
- // Heuristic: fields ending in 'Id' or '_id' containing numeric strings
330
- // Modified by allowlist (add) and blocklist (exclude)
331
- if (idType === 'number') {
332
- for (const [key, value] of Object.entries(transformed)){
333
- // Skip if not a string or already processed as a reference
334
- if (typeof value !== 'string') continue;
335
- // Check if field should be converted
336
- const matchesHeuristic = /(?:Id|_id)$/.test(key);
337
- const inAllowlist = idFieldsAllowlistSet.has(key);
338
- const inBlocklist = idFieldsBlocklistSet.has(key);
339
- if ((matchesHeuristic || inAllowlist) && !inBlocklist) {
340
- // Only convert if it's a pure numeric string
341
- if (/^\d+$/.test(value)) {
342
- transformed[key] = parseInt(value, 10);
343
- }
344
- }
345
- }
346
- }
347
- return transformed;
348
- }
349
- /**
350
237
  * Convert Better Auth where clause to Payload where clause.
351
- * Handles field name transformations for reference fields.
352
- */ function convertWhereToPayload(model, where) {
238
+ * The factory already handles field name transforms, so we just
239
+ * convert to Payload's unique where format.
240
+ */ function convertWhereToPayload(where) {
353
241
  if (!where || where.length === 0) return {};
354
242
  if (where.length === 1) {
355
243
  const w = where[0];
356
244
  return {
357
- [getPayloadFieldName(model, w.field)]: convertOperator(w.operator, w.value, resolvedDbType ?? 'postgres')
245
+ [w.field]: convertOperator(w.operator, w.value, resolvedDbType ?? 'postgres')
358
246
  };
359
247
  }
360
248
  const andConditions = where.filter((w)=>w.connector !== 'OR');
@@ -362,12 +250,12 @@
362
250
  const result = {};
363
251
  if (andConditions.length > 0) {
364
252
  result.and = andConditions.map((w)=>({
365
- [getPayloadFieldName(model, w.field)]: convertOperator(w.operator, w.value, resolvedDbType ?? 'postgres')
253
+ [w.field]: convertOperator(w.operator, w.value, resolvedDbType ?? 'postgres')
366
254
  }));
367
255
  }
368
256
  if (orConditions.length > 0) {
369
257
  result.or = orConditions.map((w)=>({
370
- [getPayloadFieldName(model, w.field)]: convertOperator(w.operator, w.value, resolvedDbType ?? 'postgres')
258
+ [w.field]: convertOperator(w.operator, w.value, resolvedDbType ?? 'postgres')
371
259
  }));
372
260
  }
373
261
  return result;
@@ -386,35 +274,31 @@
386
274
  create: async ({ model, data })=>{
387
275
  const payload = await getPayload();
388
276
  const collection = getCollection(model);
389
- const payloadData = transformDataForPayload(model, data);
390
277
  if (enableDebugLogs) {
391
278
  debugLog('create', {
392
279
  collection,
393
280
  model,
394
- data,
395
- payloadData
281
+ data
396
282
  });
397
283
  }
398
284
  try {
399
285
  const result = await payload.create({
400
286
  collection,
401
- data: payloadData,
287
+ data: data,
402
288
  depth: 0,
403
289
  // Bypass access control - Better Auth handles its own auth
404
290
  overrideAccess: true
405
291
  });
406
- // Transform back and merge with input data for Better Auth
292
+ // Merge with input data for Better Auth
407
293
  // Database result takes precedence (handles hooks that modify data like firstUserAdmin)
408
- const transformed = transformDataFromPayload(model, result);
409
294
  const merged = {
410
295
  ...data,
411
- ...transformed
296
+ ...result
412
297
  };
413
298
  if (enableDebugLogs) {
414
299
  debugLog('create result', {
415
300
  collection,
416
301
  resultId: result.id,
417
- transformedKeys: Object.keys(transformed),
418
302
  mergedKeys: Object.keys(merged)
419
303
  });
420
304
  }
@@ -446,26 +330,24 @@
446
330
  try {
447
331
  const result = await payload.findByID({
448
332
  collection,
449
- id: convertId(id),
333
+ id,
450
334
  depth: join ? 1 : 0,
451
335
  overrideAccess: true
452
336
  });
453
- const transformed = transformDataFromPayload(model, result);
454
337
  if (enableDebugLogs) {
455
338
  debugLog('findOne result (byID)', {
456
339
  collection,
457
- id: convertId(id),
458
- found: true,
459
- transformedKeys: Object.keys(transformed)
340
+ id,
341
+ found: true
460
342
  });
461
343
  }
462
- return transformed;
344
+ return result;
463
345
  } catch (error) {
464
346
  if (error instanceof Error && 'status' in error && error.status === 404) {
465
347
  if (enableDebugLogs) {
466
348
  debugLog('findOne result (byID)', {
467
349
  collection,
468
- id: convertId(id),
350
+ id,
469
351
  found: false
470
352
  });
471
353
  }
@@ -474,7 +356,7 @@
474
356
  throw error;
475
357
  }
476
358
  }
477
- const payloadWhere = convertWhereToPayload(model, where);
359
+ const payloadWhere = convertWhereToPayload(where);
478
360
  if (enableDebugLogs) {
479
361
  debugLog('findOne query', {
480
362
  collection,
@@ -498,14 +380,7 @@
498
380
  });
499
381
  }
500
382
  if (!result.docs[0]) return null;
501
- const transformed = transformDataFromPayload(model, result.docs[0]);
502
- if (enableDebugLogs) {
503
- debugLog('findOne transformed', {
504
- collection,
505
- transformedKeys: Object.keys(transformed)
506
- });
507
- }
508
- return transformed;
383
+ return result.docs[0];
509
384
  } catch (error) {
510
385
  console.error('[payload-adapter] findOne failed:', {
511
386
  model,
@@ -528,29 +403,27 @@
528
403
  sortBy
529
404
  });
530
405
  }
531
- const payloadWhere = where ? convertWhereToPayload(model, where) : {};
406
+ const payloadWhere = where ? convertWhereToPayload(where) : {};
532
407
  const result = await payload.find({
533
408
  collection,
534
409
  where: payloadWhere,
535
410
  limit: limit ?? 100,
536
411
  page: offset ? Math.floor(offset / (limit ?? 100)) + 1 : 1,
537
- sort: sortBy ? `${sortBy.direction === 'desc' ? '-' : ''}${getPayloadFieldName(model, sortBy.field)}` : undefined,
412
+ sort: sortBy ? `${sortBy.direction === 'desc' ? '-' : ''}${sortBy.field}` : undefined,
538
413
  depth: join ? 1 : 0,
539
414
  overrideAccess: true
540
415
  });
541
- return result.docs.map((doc)=>transformDataFromPayload(model, doc));
416
+ return result.docs;
542
417
  },
543
418
  update: async ({ model, where, update: data })=>{
544
419
  const payload = await getPayload();
545
420
  const collection = getCollection(model);
546
- const payloadData = transformDataForPayload(model, data);
547
421
  if (enableDebugLogs) {
548
422
  debugLog('update', {
549
423
  collection,
550
424
  model,
551
425
  where,
552
- data,
553
- payloadData
426
+ data
554
427
  });
555
428
  }
556
429
  // Optimize for single ID queries
@@ -558,50 +431,46 @@
558
431
  if (id !== null) {
559
432
  const result = await payload.update({
560
433
  collection,
561
- id: convertId(id),
562
- data: payloadData,
434
+ id,
435
+ data: data,
563
436
  depth: 0,
564
437
  overrideAccess: true
565
438
  });
566
- const transformed = transformDataFromPayload(model, result);
567
439
  return {
568
440
  ...data,
569
- ...transformed
441
+ ...result
570
442
  };
571
443
  }
572
- const payloadWhere = convertWhereToPayload(model, where);
444
+ const payloadWhere = convertWhereToPayload(where);
573
445
  const result = await payload.update({
574
446
  collection,
575
447
  where: payloadWhere,
576
- data: payloadData,
448
+ data: data,
577
449
  depth: 0,
578
450
  overrideAccess: true
579
451
  });
580
452
  if (!result.docs[0]) return null;
581
- const transformed = transformDataFromPayload(model, result.docs[0]);
582
453
  return {
583
454
  ...data,
584
- ...transformed
455
+ ...result.docs[0]
585
456
  };
586
457
  },
587
458
  updateMany: async ({ model, where, update: data })=>{
588
459
  const payload = await getPayload();
589
460
  const collection = getCollection(model);
590
- const payloadData = transformDataForPayload(model, data);
591
461
  if (enableDebugLogs) {
592
462
  debugLog('updateMany', {
593
463
  collection,
594
464
  model,
595
465
  where,
596
- data,
597
- payloadData
466
+ data
598
467
  });
599
468
  }
600
- const payloadWhere = convertWhereToPayload(model, where);
469
+ const payloadWhere = convertWhereToPayload(where);
601
470
  const result = await payload.update({
602
471
  collection,
603
472
  where: payloadWhere,
604
- data: payloadData,
473
+ data: data,
605
474
  depth: 0,
606
475
  overrideAccess: true
607
476
  });
@@ -622,12 +491,12 @@
622
491
  if (id !== null) {
623
492
  await payload.delete({
624
493
  collection,
625
- id: convertId(id),
494
+ id,
626
495
  overrideAccess: true
627
496
  });
628
497
  return;
629
498
  }
630
- const payloadWhere = convertWhereToPayload(model, where);
499
+ const payloadWhere = convertWhereToPayload(where);
631
500
  await payload.delete({
632
501
  collection,
633
502
  where: payloadWhere,
@@ -644,7 +513,7 @@
644
513
  where
645
514
  });
646
515
  }
647
- const payloadWhere = convertWhereToPayload(model, where);
516
+ const payloadWhere = convertWhereToPayload(where);
648
517
  const result = await payload.delete({
649
518
  collection,
650
519
  where: payloadWhere,
@@ -662,7 +531,7 @@
662
531
  where
663
532
  });
664
533
  }
665
- const payloadWhere = where ? convertWhereToPayload(model, where) : {};
534
+ const payloadWhere = where ? convertWhereToPayload(where) : {};
666
535
  const result = await payload.count({
667
536
  collection,
668
537
  where: payloadWhere,
@@ -198,7 +198,8 @@ import { createPayloadAuthClient } from '../../exports/client.js';
198
198
  if (result.error) {
199
199
  setError(result.error.message ?? 'Failed to load API keys');
200
200
  } else {
201
- setApiKeys(result.data ?? []);
201
+ const data = result.data;
202
+ setApiKeys(Array.isArray(data) ? data : data.apiKeys ?? []);
202
203
  }
203
204
  } catch {
204
205
  setError('Failed to load API keys');