@delmaredigital/payload-better-auth 0.4.4 → 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/README.md CHANGED
@@ -28,7 +28,7 @@ For AI-assisted exploration: [DeepWiki](https://deepwiki.com/delmaredigital/payl
28
28
  pnpm add @delmaredigital/payload-better-auth better-auth
29
29
  ```
30
30
 
31
- **Requirements:** `payload` >= 3.69.0 · `better-auth` >= 1.4.0 · `next` >= 15.4.8 · `react` >= 19.2.1
31
+ **Requirements:** `payload` >= 3.69.0 · `better-auth` >= 1.5.0 · `next` >= 15.4.8 · `react` >= 19.2.1
32
32
 
33
33
  ## Quick Start
34
34
 
@@ -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;
@@ -213,22 +211,21 @@
213
211
  }
214
212
  return resolvePromise;
215
213
  };
216
- // Helper to convert ID based on type
217
- const convertId = (id)=>{
218
- if (idType === 'number' && typeof id === 'string') {
219
- const num = parseInt(id, 10);
220
- return isNaN(num) ? id : num;
221
- }
222
- if (idType === 'text' && typeof id === 'number') {
223
- return String(id);
224
- }
225
- return id;
226
- };
227
214
  // Create the adapter using createAdapterFactory
228
215
  // The factory handles all schema-aware transformations for us
229
216
  const adapterFactory = createAdapterFactory({
230
217
  config: factoryConfig,
231
- 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
+ }
232
229
  // Log initialization
233
230
  if (enableDebugLogs) {
234
231
  debugLog('Adapter initialized', {
@@ -237,126 +234,15 @@
237
234
  });
238
235
  }
239
236
  /**
240
- * Get the schema for a model, handling plural/singular lookups.
241
- * Better Auth queries with plural names when usePlural is true,
242
- * but schema keys are singular.
243
- */ function getModelSchema(model) {
244
- // First try direct lookup
245
- if (schema[model]) return schema[model];
246
- // Try singular form (strip trailing 's') for plural model names
247
- const singular = model.endsWith('s') ? model.slice(0, -1) : model;
248
- if (schema[singular]) return schema[singular];
249
- // Try without 'ies' → 'y' conversion (e.g., 'verifications' → 'verification')
250
- // This handles edge cases but 'verifications' → 'verification' works with simple 's' strip
251
- return undefined;
252
- }
253
- /**
254
- * Transform a Better Auth field name to a Payload field name.
255
- *
256
- * For reference fields (those with `references` in schema), Payload collections
257
- * use the field name without the `Id`/`_id` suffix (e.g., `userId` → `user`).
258
- * This matches how betterAuthCollections() generates relationship fields.
259
- */ function getPayloadFieldName(model, field) {
260
- // First apply any custom field name mappings from BetterAuthOptions
261
- const mappedField = getFieldName({
262
- model,
263
- field
264
- });
265
- // Check if this field is a reference field in the schema
266
- const modelSchema = getModelSchema(model);
267
- if (modelSchema?.fields?.[field]?.references) {
268
- // Strip _id or Id suffix for reference fields
269
- // This matches betterAuthCollections() which does: fieldName.replace(/(_id|Id)$/, '')
270
- return mappedField.replace(/(_id|Id)$/, '');
271
- }
272
- return mappedField;
273
- }
274
- /**
275
- * Transform input data from Better Auth format to Payload format.
276
- * Converts reference field names (e.g., `userId` → `user`) and
277
- * converts reference field values to the correct ID type.
278
- */ function transformDataForPayload(model, data) {
279
- const modelSchema = getModelSchema(model);
280
- if (!modelSchema?.fields) return data;
281
- const transformed = {};
282
- for (const [key, value] of Object.entries(data)){
283
- const payloadKey = getPayloadFieldName(model, key);
284
- let transformedValue = value;
285
- // If this is a reference field, convert the ID to the correct type
286
- const fieldDef = modelSchema.fields[key];
287
- if (fieldDef?.references && value !== null && value !== undefined) {
288
- // Convert reference ID to the correct type (number for SERIAL, string for UUID)
289
- if (idType === 'number' && typeof value === 'string') {
290
- const numValue = parseInt(value, 10);
291
- if (!isNaN(numValue)) {
292
- transformedValue = numValue;
293
- }
294
- } else if (idType === 'text' && typeof value === 'number') {
295
- transformedValue = String(value);
296
- }
297
- }
298
- transformed[payloadKey] = transformedValue;
299
- }
300
- if (enableDebugLogs) {
301
- console.log('[payload-adapter] transformDataForPayload:', {
302
- model,
303
- inputKeys: Object.keys(data),
304
- outputKeys: Object.keys(transformed),
305
- transformedData: transformed
306
- });
307
- }
308
- return transformed;
309
- }
310
- /**
311
- * Transform output data from Payload format to Better Auth format.
312
- * Converts reference field names back (e.g., `user` → `userId`).
313
- */ function transformDataFromPayload(model, data) {
314
- const modelSchema = getModelSchema(model);
315
- if (!modelSchema?.fields || !data) return data;
316
- const transformed = {
317
- ...data
318
- };
319
- // For each field in the schema that has references,
320
- // check if Payload returned the stripped name and map it back
321
- for (const [fieldKey, fieldDef] of Object.entries(modelSchema.fields)){
322
- if (fieldDef.references) {
323
- const payloadFieldName = fieldKey.replace(/(_id|Id)$/, '');
324
- if (payloadFieldName in data && !(fieldKey in transformed)) {
325
- transformed[fieldKey] = data[payloadFieldName];
326
- // Keep both for compatibility - Better Auth expects userId
327
- }
328
- }
329
- }
330
- // Convert semantic ID fields to numbers when using serial IDs
331
- // Heuristic: fields ending in 'Id' or '_id' containing numeric strings
332
- // Modified by allowlist (add) and blocklist (exclude)
333
- if (idType === 'number') {
334
- for (const [key, value] of Object.entries(transformed)){
335
- // Skip if not a string or already processed as a reference
336
- if (typeof value !== 'string') continue;
337
- // Check if field should be converted
338
- const matchesHeuristic = /(?:Id|_id)$/.test(key);
339
- const inAllowlist = idFieldsAllowlistSet.has(key);
340
- const inBlocklist = idFieldsBlocklistSet.has(key);
341
- if ((matchesHeuristic || inAllowlist) && !inBlocklist) {
342
- // Only convert if it's a pure numeric string
343
- if (/^\d+$/.test(value)) {
344
- transformed[key] = parseInt(value, 10);
345
- }
346
- }
347
- }
348
- }
349
- return transformed;
350
- }
351
- /**
352
237
  * Convert Better Auth where clause to Payload where clause.
353
- * Handles field name transformations for reference fields.
354
- */ 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) {
355
241
  if (!where || where.length === 0) return {};
356
242
  if (where.length === 1) {
357
243
  const w = where[0];
358
244
  return {
359
- [getPayloadFieldName(model, w.field)]: convertOperator(w.operator, w.value, resolvedDbType ?? 'postgres')
245
+ [w.field]: convertOperator(w.operator, w.value, resolvedDbType ?? 'postgres')
360
246
  };
361
247
  }
362
248
  const andConditions = where.filter((w)=>w.connector !== 'OR');
@@ -364,12 +250,12 @@
364
250
  const result = {};
365
251
  if (andConditions.length > 0) {
366
252
  result.and = andConditions.map((w)=>({
367
- [getPayloadFieldName(model, w.field)]: convertOperator(w.operator, w.value, resolvedDbType ?? 'postgres')
253
+ [w.field]: convertOperator(w.operator, w.value, resolvedDbType ?? 'postgres')
368
254
  }));
369
255
  }
370
256
  if (orConditions.length > 0) {
371
257
  result.or = orConditions.map((w)=>({
372
- [getPayloadFieldName(model, w.field)]: convertOperator(w.operator, w.value, resolvedDbType ?? 'postgres')
258
+ [w.field]: convertOperator(w.operator, w.value, resolvedDbType ?? 'postgres')
373
259
  }));
374
260
  }
375
261
  return result;
@@ -388,35 +274,31 @@
388
274
  create: async ({ model, data })=>{
389
275
  const payload = await getPayload();
390
276
  const collection = getCollection(model);
391
- const payloadData = transformDataForPayload(model, data);
392
277
  if (enableDebugLogs) {
393
278
  debugLog('create', {
394
279
  collection,
395
280
  model,
396
- data,
397
- payloadData
281
+ data
398
282
  });
399
283
  }
400
284
  try {
401
285
  const result = await payload.create({
402
286
  collection,
403
- data: payloadData,
287
+ data: data,
404
288
  depth: 0,
405
289
  // Bypass access control - Better Auth handles its own auth
406
290
  overrideAccess: true
407
291
  });
408
- // Transform back and merge with input data for Better Auth
292
+ // Merge with input data for Better Auth
409
293
  // Database result takes precedence (handles hooks that modify data like firstUserAdmin)
410
- const transformed = transformDataFromPayload(model, result);
411
294
  const merged = {
412
295
  ...data,
413
- ...transformed
296
+ ...result
414
297
  };
415
298
  if (enableDebugLogs) {
416
299
  debugLog('create result', {
417
300
  collection,
418
301
  resultId: result.id,
419
- transformedKeys: Object.keys(transformed),
420
302
  mergedKeys: Object.keys(merged)
421
303
  });
422
304
  }
@@ -448,26 +330,24 @@
448
330
  try {
449
331
  const result = await payload.findByID({
450
332
  collection,
451
- id: convertId(id),
333
+ id,
452
334
  depth: join ? 1 : 0,
453
335
  overrideAccess: true
454
336
  });
455
- const transformed = transformDataFromPayload(model, result);
456
337
  if (enableDebugLogs) {
457
338
  debugLog('findOne result (byID)', {
458
339
  collection,
459
- id: convertId(id),
460
- found: true,
461
- transformedKeys: Object.keys(transformed)
340
+ id,
341
+ found: true
462
342
  });
463
343
  }
464
- return transformed;
344
+ return result;
465
345
  } catch (error) {
466
346
  if (error instanceof Error && 'status' in error && error.status === 404) {
467
347
  if (enableDebugLogs) {
468
348
  debugLog('findOne result (byID)', {
469
349
  collection,
470
- id: convertId(id),
350
+ id,
471
351
  found: false
472
352
  });
473
353
  }
@@ -476,7 +356,7 @@
476
356
  throw error;
477
357
  }
478
358
  }
479
- const payloadWhere = convertWhereToPayload(model, where);
359
+ const payloadWhere = convertWhereToPayload(where);
480
360
  if (enableDebugLogs) {
481
361
  debugLog('findOne query', {
482
362
  collection,
@@ -500,14 +380,7 @@
500
380
  });
501
381
  }
502
382
  if (!result.docs[0]) return null;
503
- const transformed = transformDataFromPayload(model, result.docs[0]);
504
- if (enableDebugLogs) {
505
- debugLog('findOne transformed', {
506
- collection,
507
- transformedKeys: Object.keys(transformed)
508
- });
509
- }
510
- return transformed;
383
+ return result.docs[0];
511
384
  } catch (error) {
512
385
  console.error('[payload-adapter] findOne failed:', {
513
386
  model,
@@ -530,29 +403,27 @@
530
403
  sortBy
531
404
  });
532
405
  }
533
- const payloadWhere = where ? convertWhereToPayload(model, where) : {};
406
+ const payloadWhere = where ? convertWhereToPayload(where) : {};
534
407
  const result = await payload.find({
535
408
  collection,
536
409
  where: payloadWhere,
537
410
  limit: limit ?? 100,
538
411
  page: offset ? Math.floor(offset / (limit ?? 100)) + 1 : 1,
539
- sort: sortBy ? `${sortBy.direction === 'desc' ? '-' : ''}${getPayloadFieldName(model, sortBy.field)}` : undefined,
412
+ sort: sortBy ? `${sortBy.direction === 'desc' ? '-' : ''}${sortBy.field}` : undefined,
540
413
  depth: join ? 1 : 0,
541
414
  overrideAccess: true
542
415
  });
543
- return result.docs.map((doc)=>transformDataFromPayload(model, doc));
416
+ return result.docs;
544
417
  },
545
418
  update: async ({ model, where, update: data })=>{
546
419
  const payload = await getPayload();
547
420
  const collection = getCollection(model);
548
- const payloadData = transformDataForPayload(model, data);
549
421
  if (enableDebugLogs) {
550
422
  debugLog('update', {
551
423
  collection,
552
424
  model,
553
425
  where,
554
- data,
555
- payloadData
426
+ data
556
427
  });
557
428
  }
558
429
  // Optimize for single ID queries
@@ -560,50 +431,46 @@
560
431
  if (id !== null) {
561
432
  const result = await payload.update({
562
433
  collection,
563
- id: convertId(id),
564
- data: payloadData,
434
+ id,
435
+ data: data,
565
436
  depth: 0,
566
437
  overrideAccess: true
567
438
  });
568
- const transformed = transformDataFromPayload(model, result);
569
439
  return {
570
440
  ...data,
571
- ...transformed
441
+ ...result
572
442
  };
573
443
  }
574
- const payloadWhere = convertWhereToPayload(model, where);
444
+ const payloadWhere = convertWhereToPayload(where);
575
445
  const result = await payload.update({
576
446
  collection,
577
447
  where: payloadWhere,
578
- data: payloadData,
448
+ data: data,
579
449
  depth: 0,
580
450
  overrideAccess: true
581
451
  });
582
452
  if (!result.docs[0]) return null;
583
- const transformed = transformDataFromPayload(model, result.docs[0]);
584
453
  return {
585
454
  ...data,
586
- ...transformed
455
+ ...result.docs[0]
587
456
  };
588
457
  },
589
458
  updateMany: async ({ model, where, update: data })=>{
590
459
  const payload = await getPayload();
591
460
  const collection = getCollection(model);
592
- const payloadData = transformDataForPayload(model, data);
593
461
  if (enableDebugLogs) {
594
462
  debugLog('updateMany', {
595
463
  collection,
596
464
  model,
597
465
  where,
598
- data,
599
- payloadData
466
+ data
600
467
  });
601
468
  }
602
- const payloadWhere = convertWhereToPayload(model, where);
469
+ const payloadWhere = convertWhereToPayload(where);
603
470
  const result = await payload.update({
604
471
  collection,
605
472
  where: payloadWhere,
606
- data: payloadData,
473
+ data: data,
607
474
  depth: 0,
608
475
  overrideAccess: true
609
476
  });
@@ -624,12 +491,12 @@
624
491
  if (id !== null) {
625
492
  await payload.delete({
626
493
  collection,
627
- id: convertId(id),
494
+ id,
628
495
  overrideAccess: true
629
496
  });
630
497
  return;
631
498
  }
632
- const payloadWhere = convertWhereToPayload(model, where);
499
+ const payloadWhere = convertWhereToPayload(where);
633
500
  await payload.delete({
634
501
  collection,
635
502
  where: payloadWhere,
@@ -646,7 +513,7 @@
646
513
  where
647
514
  });
648
515
  }
649
- const payloadWhere = convertWhereToPayload(model, where);
516
+ const payloadWhere = convertWhereToPayload(where);
650
517
  const result = await payload.delete({
651
518
  collection,
652
519
  where: payloadWhere,
@@ -664,7 +531,7 @@
664
531
  where
665
532
  });
666
533
  }
667
- const payloadWhere = where ? convertWhereToPayload(model, where) : {};
534
+ const payloadWhere = where ? convertWhereToPayload(where) : {};
668
535
  const result = await payload.count({
669
536
  collection,
670
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');