@framers/sql-storage-adapter 0.7.3 → 0.7.5

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.
Files changed (45) hide show
  1. package/README.md +45 -13
  2. package/dist/cjs/core/contracts/features.js +4 -2
  3. package/dist/cjs/core/contracts/fts.d.ts +20 -8
  4. package/dist/cjs/core/portable.d.ts +2 -0
  5. package/dist/cjs/core/portable.js +5 -0
  6. package/dist/cjs/features/auth/authJsAdapter.d.ts +110 -0
  7. package/dist/cjs/features/auth/authJsAdapter.js +251 -0
  8. package/dist/cjs/fts/PostgresFts.d.ts +14 -13
  9. package/dist/cjs/fts/PostgresFts.js +61 -33
  10. package/dist/cjs/fts/SqliteFts4.d.ts +28 -0
  11. package/dist/cjs/fts/SqliteFts4.js +67 -0
  12. package/dist/cjs/fts/SqliteFts5.d.ts +1 -0
  13. package/dist/cjs/fts/SqliteFts5.js +3 -0
  14. package/dist/cjs/index.d.ts +2 -0
  15. package/dist/cjs/index.js +5 -0
  16. package/dist/core/contracts/features.d.ts.map +1 -1
  17. package/dist/core/contracts/features.js +6 -2
  18. package/dist/core/contracts/features.js.map +1 -1
  19. package/dist/core/contracts/fts.d.ts +20 -8
  20. package/dist/core/contracts/fts.d.ts.map +1 -1
  21. package/dist/core/portable.d.ts +2 -0
  22. package/dist/core/portable.d.ts.map +1 -1
  23. package/dist/core/portable.js +2 -0
  24. package/dist/core/portable.js.map +1 -1
  25. package/dist/features/auth/authJsAdapter.d.ts +110 -0
  26. package/dist/features/auth/authJsAdapter.d.ts.map +1 -0
  27. package/dist/features/auth/authJsAdapter.js +248 -0
  28. package/dist/features/auth/authJsAdapter.js.map +1 -0
  29. package/dist/fts/PostgresFts.d.ts +14 -13
  30. package/dist/fts/PostgresFts.d.ts.map +1 -1
  31. package/dist/fts/PostgresFts.js +83 -39
  32. package/dist/fts/PostgresFts.js.map +1 -1
  33. package/dist/fts/SqliteFts4.d.ts +28 -0
  34. package/dist/fts/SqliteFts4.d.ts.map +1 -0
  35. package/dist/fts/SqliteFts4.js +56 -0
  36. package/dist/fts/SqliteFts4.js.map +1 -0
  37. package/dist/fts/SqliteFts5.d.ts +1 -0
  38. package/dist/fts/SqliteFts5.d.ts.map +1 -1
  39. package/dist/fts/SqliteFts5.js +3 -0
  40. package/dist/fts/SqliteFts5.js.map +1 -1
  41. package/dist/index.d.ts +2 -0
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +2 -0
  44. package/dist/index.js.map +1 -1
  45. package/package.json +11 -1
package/README.md CHANGED
@@ -93,6 +93,7 @@ import { createStorageFeatures, runSchemaMigrations, isUniqueViolation } from '@
93
93
  | `./core` | Contracts and errors, dialects, full-text search, codecs, exporters, `createStorageFeatures`, the schema-migration runner, `isUniqueViolation`, the logger. No adapter |
94
94
  | `./postgres`, `./better-sqlite3`, `./sqljs`, `./indexeddb`, `./capacitor` | One adapter each |
95
95
  | `./backup` | S3-compatible backups (`@aws-sdk/client-s3`) |
96
+ | `./authjs` | The Auth.js database adapter (also in `./core`) |
96
97
  | `./electron`, `./electron/renderer`, `./electron/preload` | The Electron adapters. `./electron/renderer` holds only what a window uses; `require('./electron/preload')` gives one bundled CommonJS file for a sandboxed preload |
97
98
  | `./sync`, `./kv`, `./types` | Cross-platform sync, the key-value store, the types |
98
99
 
@@ -381,6 +382,26 @@ const status = await schemaMigrationStatus(db, migrations); // { appl
381
382
  - **Outside a transaction.** A file whose first line is `-- outside transaction` (`outsideTransactionMarker`) runs outside a transaction, for `CREATE INDEX CONCURRENTLY` or `VACUUM`. On PostgreSQL it runs under the session lock on one held connection (`withConnection`).
382
383
  - **Unknown rows.** `unknown: 'ignore'` lets a run go on past ledger rows the list lacks.
383
384
 
385
+ ### Auth.js
386
+
387
+ `createAuthJsAdapter(db)` is an Auth.js database adapter. It keeps users, accounts, sessions and verification tokens in the product's own database through this library, on PostgreSQL or SQLite. Pass it to NextAuth v5 or `@auth/core`; it has the shape of `Adapter` from `@auth/core/adapters`, which CI checks, and needs no dependency on it.
388
+
389
+ ```typescript
390
+ import NextAuth from 'next-auth';
391
+ import { createDatabase } from '@framers/sql-storage-adapter';
392
+ import { createAuthJsAdapter, ensureAuthJsTables } from '@framers/sql-storage-adapter/authjs';
393
+
394
+ const db = await createDatabase({ url: process.env.DATABASE_URL, strict: true });
395
+ await ensureAuthJsTables(db); // or put authJsTablesSql(db) in a migration
396
+ export const { handlers, auth, signIn, signOut } = NextAuth({ adapter: createAuthJsAdapter(db), providers: [/* ... */] });
397
+ ```
398
+
399
+ - **Tables.** They are `auth_users`, `auth_accounts`, `auth_sessions` and `auth_verification_tokens`; `tablePrefix` changes the `auth_`. Ids are text, from `crypto.randomUUID()` unless Auth.js passes one or `generateId` is given. Times are `timestamptz` on PostgreSQL and ISO 8601 text on SQLite, and Dates either way in JavaScript.
400
+ - **Deletes.** `deleteUser` deletes the user's sessions and accounts first, in one transaction, so it holds on engines that leave foreign keys off.
401
+ - **Verification tokens.** `useVerificationToken` deletes and returns the token in one statement (`DELETE ... RETURNING`), so a token works once.
402
+ - **Passkeys.** WebAuthn authenticators are not stored.
403
+ - **Tests.** Auth.js's own adapter test suite runs against it on PostgreSQL, better-sqlite3 and sql.js.
404
+
384
405
  ### Values
385
406
 
386
407
  On SQLite, values convert as they bind, positional and named alike:
@@ -709,30 +730,41 @@ const ddl = `CREATE TABLE notes (id ${d.columnType('uuid')} PRIMARY KEY DEFAULT
709
730
  - Column values follow each engine: a PostgreSQL `BOOLEAN` returns `true` and the SQLite `INTEGER` returns `1`, and a `TIMESTAMPTZ` returns a `Date` where SQLite returns text. `typeParsers` (see "Values") narrows the PostgreSQL side.
710
731
  - `pragma()` returns null on PostgreSQL and `advisoryTransactionLock()` returns null on SQLite: skip the statement when it is null.
711
732
 
712
- ### IFullTextSearch — FTS5 & tsvector
733
+ ### IFullTextSearch — FTS5, FTS4 & tsvector
734
+
735
+ `createStorageFeatures` gives each engine the full-text search it has:
736
+ - FTS5 on better-sqlite3;
737
+ - FTS4 on sql.js and IndexedDB, whose stock sql.js build compiles FTS3 and FTS4 and no FTS5;
738
+ - a tsvector column with a GIN index on PostgreSQL, the Supabase adapter included.
739
+
740
+ The same calls return the same rows on all three, and `tests/fts.parity.spec.ts` runs them on each.
713
741
 
714
742
  ```typescript
715
743
  const { fts } = features;
716
744
 
717
- // Create index: FTS5 virtual table (SQLite) or tsvector + GIN (Postgres)
718
- await adapter.exec(fts.createIndex({
745
+ // FTS5 or FTS4 virtual table (SQLite), or a _tsv column with a GIN index (PostgreSQL)
746
+ await db.exec(fts.createIndex({
719
747
  table: 'docs_fts',
720
748
  columns: ['title', 'body'],
721
749
  contentTable: 'documents',
722
750
  tokenizer: 'porter ascii',
751
+ weights: { title: 'A', body: 'C' }, // PostgreSQL ranks with them; SQLite ignores them
723
752
  }));
753
+ await db.exec(fts.rebuildCommand('docs_fts'));
754
+
755
+ // The best match first on every engine; a named placeholder binds once wherever it appears
756
+ const rows = await db.all(
757
+ `SELECT t.id FROM ${fts.joinClause('documents', 't', 'docs_fts', 'docs_fts')}
758
+ WHERE ${fts.matchClause('docs_fts', '@q')} ORDER BY ${fts.relevanceOrder('docs_fts', '@q')}`,
759
+ { q: fts.sanitizeQuery(userText) },
760
+ );
761
+ ```
724
762
 
725
- // Search query
726
- const sql = `
727
- SELECT t.*
728
- FROM ${fts.joinClause('documents', 't', 'fts', 'docs_fts')}
729
- WHERE ${fts.matchClause('docs_fts', '?')}
730
- ORDER BY ${fts.rankExpression('fts')}
731
- `;
763
+ - **Ordering.** `relevanceOrder` orders best first everywhere. `rankExpression` keeps each engine's own direction: FTS5's `rank` is lower-better, FTS4's `length(offsets())` and PostgreSQL's `ts_rank` are higher-better.
764
+ - **Queries.** `sanitizeQuery` turns text into its words, quoted and joined with OR, on every engine. PostgreSQL reads queries with `websearch_to_tsquery`: words are ANDed, and `or`, `-word` and quoted phrases work as in a search box.
765
+ - **One index, many features objects.** On PostgreSQL, `createIndex` records each index's table, columns, language and weights for the whole process, so a features object made later syncs, rebuilds and queries an index another one created, and each index keeps its own language. Run `createIndex` (it is idempotent) in each process before `syncInsert` or `rebuildCommand`.
766
+ - **Aliases.** On SQLite, alias the index as itself in `joinClause` (or pass the alias as `indexName` to `matchClause`), since a MATCH names the table as the query writes it.
732
767
 
733
- // Rebuild index
734
- await adapter.exec(fts.rebuildCommand('docs_fts'));
735
- ```
736
768
 
737
769
  ### IBlobCodec — Cross-Platform Binary Encoding
738
770
 
@@ -23,6 +23,7 @@ module.exports = __toCommonJS(features_exports);
23
23
  var import_SqliteDialect = require("../../dialects/SqliteDialect.js");
24
24
  var import_PostgresDialect = require("../../dialects/PostgresDialect.js");
25
25
  var import_SqliteFts5 = require("../../fts/SqliteFts5.js");
26
+ var import_SqliteFts4 = require("../../fts/SqliteFts4.js");
26
27
  var import_PostgresFts = require("../../fts/PostgresFts.js");
27
28
  var import_NodeBlobCodec = require("../../codecs/NodeBlobCodec.js");
28
29
  var import_BrowserBlobCodec = require("../../codecs/BrowserBlobCodec.js");
@@ -45,12 +46,13 @@ function extractPostgresConnectionString(adapter) {
45
46
  return `postgresql://${auth}${host}:${port}/${options.database}${sslQuery}`;
46
47
  }
47
48
  function createStorageFeatures(adapter) {
48
- const isPostgres = adapter.kind === "postgres";
49
+ const isPostgres = adapter.kind === "postgres" || adapter.kind === "supabase-postgres";
50
+ const hasFts5 = adapter.kind !== "sqljs" && adapter.kind !== "indexeddb";
49
51
  const isBrowser = typeof window !== "undefined" && typeof window.document !== "undefined";
50
52
  const postgresConnectionString = isPostgres ? extractPostgresConnectionString(adapter) : void 0;
51
53
  return {
52
54
  dialect: isPostgres ? new import_PostgresDialect.PostgresDialect() : new import_SqliteDialect.SqliteDialect(),
53
- fts: isPostgres ? new import_PostgresFts.PostgresFts() : new import_SqliteFts5.SqliteFts5(),
55
+ fts: isPostgres ? new import_PostgresFts.PostgresFts() : hasFts5 ? new import_SqliteFts5.SqliteFts5() : new import_SqliteFts4.SqliteFts4(),
54
56
  blobCodec: isBrowser ? new import_BrowserBlobCodec.BrowserBlobCodec() : new import_NodeBlobCodec.NodeBlobCodec(),
55
57
  exporter: isPostgres ? new import_PostgresExporter.PostgresExporter(postgresConnectionString) : new import_SqliteFileExporter.SqliteFileExporter(adapter)
56
58
  };
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Full-text search abstraction.
3
3
  *
4
- * SQLite implementations use FTS5 virtual tables.
5
- * Postgres implementations use tsvector columns with GIN indexes.
4
+ * better-sqlite3 uses FTS5 virtual tables; sql.js and IndexedDB use FTS4, which
5
+ * their stock sql.js build has (it has no FTS5). Postgres implementations use
6
+ * tsvector columns with GIN indexes.
6
7
  * Methods return SQL strings — no database calls.
7
8
  */
8
9
  export interface IFullTextSearch {
@@ -13,25 +14,36 @@ export interface IFullTextSearch {
13
14
  * @param config.columns - Columns to index.
14
15
  * @param config.contentTable - Source table (for external-content FTS5).
15
16
  * @param config.tokenizer - Tokenizer config (e.g. 'porter ascii').
17
+ * @param config.weights - PostgreSQL: a weight per column (A highest, D lowest) for ranking. The SQLite engines rank without it.
16
18
  */
17
19
  createIndex(config: {
18
20
  table: string;
19
21
  columns: string[];
20
22
  contentTable?: string;
21
23
  tokenizer?: string;
24
+ weights?: Record<string, 'A' | 'B' | 'C' | 'D'>;
22
25
  }): string;
23
26
  /**
24
27
  * Generate a WHERE clause fragment for full-text matching.
25
28
  * SQLite: `memory_traces_fts MATCH ?`
26
- * Postgres: `memory_traces._tsv @@ plainto_tsquery('english', $1)`
29
+ * Postgres: `_tsv @@ websearch_to_tsquery('english', $1)`, in the index's own language
27
30
  */
28
31
  matchClause(indexName: string, queryPlaceholder: string): string;
29
32
  /**
30
- * Generate an ORDER BY rank expression.
31
- * SQLite: `memory_traces_fts.rank`
32
- * Postgres: `ts_rank(memory_traces._tsv, plainto_tsquery('english', $1))`
33
+ * A rank value, in each engine's own direction.
34
+ * SQLite FTS5: `memory_traces_fts.rank` (lower is better)
35
+ * SQLite FTS4: `length(offsets(memory_traces_fts))` (higher is better)
36
+ * Postgres: `ts_rank(_tsv, websearch_to_tsquery('english', $1))` (higher is better)
37
+ * Use relevanceOrder to order the same way on every engine.
33
38
  */
34
39
  rankExpression(indexName: string, queryPlaceholder?: string): string;
40
+ /**
41
+ * An ORDER BY fragment that puts the best match first on every engine.
42
+ * SQLite FTS5: `memory_traces_fts.rank`
43
+ * SQLite FTS4: `length(offsets(memory_traces_fts)) DESC`
44
+ * Postgres: `ts_rank(_tsv, websearch_to_tsquery('english', $1)) DESC`
45
+ */
46
+ relevanceOrder(indexName: string, queryPlaceholder?: string): string;
35
47
  /**
36
48
  * Generate the rebuild/reindex command.
37
49
  * SQLite: `INSERT INTO fts_table(fts_table) VALUES('rebuild')`
@@ -46,8 +58,8 @@ export interface IFullTextSearch {
46
58
  syncInsert(indexName: string, rowIdExpr: string, columns: string[]): string;
47
59
  /**
48
60
  * Sanitize natural-language input into a safe search query.
49
- * SQLite: wraps words in quotes, strips FTS5 operators.
50
- * Postgres: pass-through (plainto_tsquery handles it).
61
+ * Every engine: the words, quoted, joined with OR, with the operators of
62
+ * the query syntax stripped, so it matches the same rows everywhere.
51
63
  */
52
64
  sanitizeQuery(input: string): string;
53
65
  /**
@@ -10,6 +10,7 @@ export * from '../types/index.js';
10
10
  export { SqliteDialect } from '../dialects/SqliteDialect.js';
11
11
  export { PostgresDialect } from '../dialects/PostgresDialect.js';
12
12
  export { SqliteFts5 } from '../fts/SqliteFts5.js';
13
+ export { SqliteFts4 } from '../fts/SqliteFts4.js';
13
14
  export { PostgresFts } from '../fts/PostgresFts.js';
14
15
  export { NodeBlobCodec } from '../codecs/NodeBlobCodec.js';
15
16
  export { BrowserBlobCodec } from '../codecs/BrowserBlobCodec.js';
@@ -19,5 +20,6 @@ export { createStorageFeatures } from './contracts/features.js';
19
20
  export { isUniqueViolation } from '../shared/errors.js';
20
21
  export { setStorageLogger, getStorageLogger, type StorageLogger } from '../shared/logger.js';
21
22
  export * from '../features/migrations/schemaMigrations.js';
23
+ export * from '../features/auth/authJsAdapter.js';
22
24
  export * from '../shared/parameterUtils.js';
23
25
  //# sourceMappingURL=portable.d.ts.map
@@ -25,6 +25,7 @@ __export(portable_exports, {
25
25
  PostgresFts: () => import_PostgresFts.PostgresFts,
26
26
  SqliteDialect: () => import_SqliteDialect.SqliteDialect,
27
27
  SqliteFileExporter: () => import_SqliteFileExporter.SqliteFileExporter,
28
+ SqliteFts4: () => import_SqliteFts4.SqliteFts4,
28
29
  SqliteFts5: () => import_SqliteFts5.SqliteFts5,
29
30
  createStorageFeatures: () => import_features.createStorageFeatures,
30
31
  getStorageLogger: () => import_logger.getStorageLogger,
@@ -36,6 +37,7 @@ __reExport(portable_exports, require("../types/index.js"), module.exports);
36
37
  var import_SqliteDialect = require("../dialects/SqliteDialect.js");
37
38
  var import_PostgresDialect = require("../dialects/PostgresDialect.js");
38
39
  var import_SqliteFts5 = require("../fts/SqliteFts5.js");
40
+ var import_SqliteFts4 = require("../fts/SqliteFts4.js");
39
41
  var import_PostgresFts = require("../fts/PostgresFts.js");
40
42
  var import_NodeBlobCodec = require("../codecs/NodeBlobCodec.js");
41
43
  var import_BrowserBlobCodec = require("../codecs/BrowserBlobCodec.js");
@@ -45,6 +47,7 @@ var import_features = require("./contracts/features.js");
45
47
  var import_errors = require("../shared/errors.js");
46
48
  var import_logger = require("../shared/logger.js");
47
49
  __reExport(portable_exports, require("../features/migrations/schemaMigrations.js"), module.exports);
50
+ __reExport(portable_exports, require("../features/auth/authJsAdapter.js"), module.exports);
48
51
  __reExport(portable_exports, require("../shared/parameterUtils.js"), module.exports);
49
52
  // Annotate the CommonJS export names for ESM import in node:
50
53
  0 && (module.exports = {
@@ -55,6 +58,7 @@ __reExport(portable_exports, require("../shared/parameterUtils.js"), module.expo
55
58
  PostgresFts,
56
59
  SqliteDialect,
57
60
  SqliteFileExporter,
61
+ SqliteFts4,
58
62
  SqliteFts5,
59
63
  createStorageFeatures,
60
64
  getStorageLogger,
@@ -62,5 +66,6 @@ __reExport(portable_exports, require("../shared/parameterUtils.js"), module.expo
62
66
  setStorageLogger,
63
67
  ...require("../types/index.js"),
64
68
  ...require("../features/migrations/schemaMigrations.js"),
69
+ ...require("../features/auth/authJsAdapter.js"),
65
70
  ...require("../shared/parameterUtils.js")
66
71
  });
@@ -0,0 +1,110 @@
1
+ /**
2
+ * An Auth.js database adapter over StorageAdapter: users, accounts, sessions
3
+ * and verification tokens kept through this library on PostgreSQL and SQLite
4
+ * alike, so a product's own Auth.js (NextAuth v5, `@auth/core`) stores its
5
+ * auth data where the rest of its data lives.
6
+ *
7
+ * The object `createAuthJsAdapter()` returns has the shape of `Adapter` from
8
+ * `@auth/core/adapters`, without a dependency on it: pass it as `adapter` to
9
+ * `NextAuth()` or `Auth()`. WebAuthn authenticators are not stored.
10
+ *
11
+ * @module
12
+ */
13
+ import type { StorageAdapter } from '../../core/contracts/index.js';
14
+ /** A user as Auth.js reads it (`AdapterUser`). */
15
+ export interface AuthJsUser {
16
+ id: string;
17
+ email: string;
18
+ emailVerified: Date | null;
19
+ name?: string | null;
20
+ image?: string | null;
21
+ }
22
+ /**
23
+ * An account as Auth.js reads it (`AdapterAccount`). A type, not an
24
+ * interface, so it fits Auth.js's account, which allows more token fields.
25
+ * Fields the provider did not give are left out.
26
+ */
27
+ export type AuthJsAccount = {
28
+ userId: string;
29
+ type: 'oauth' | 'oidc' | 'email' | 'webauthn';
30
+ provider: string;
31
+ providerAccountId: string;
32
+ refresh_token?: string;
33
+ access_token?: string;
34
+ expires_at?: number;
35
+ token_type?: Lowercase<string>;
36
+ scope?: string;
37
+ id_token?: string;
38
+ session_state?: string;
39
+ };
40
+ /** A session as Auth.js reads it (`AdapterSession`). */
41
+ export interface AuthJsSession {
42
+ sessionToken: string;
43
+ userId: string;
44
+ expires: Date;
45
+ }
46
+ /** A verification token as Auth.js reads it (`VerificationToken`). */
47
+ export interface AuthJsVerificationToken {
48
+ identifier: string;
49
+ token: string;
50
+ expires: Date;
51
+ }
52
+ /** The methods of Auth.js's `Adapter` this adapter implements. */
53
+ export interface AuthJsAdapter {
54
+ createUser(user: Omit<AuthJsUser, 'id'> & {
55
+ id?: string;
56
+ }): Promise<AuthJsUser>;
57
+ getUser(id: string): Promise<AuthJsUser | null>;
58
+ getUserByEmail(email: string): Promise<AuthJsUser | null>;
59
+ getUserByAccount(account: Pick<AuthJsAccount, 'provider' | 'providerAccountId'>): Promise<AuthJsUser | null>;
60
+ updateUser(user: Partial<AuthJsUser> & Pick<AuthJsUser, 'id'>): Promise<AuthJsUser>;
61
+ deleteUser(userId: string): Promise<AuthJsUser | null>;
62
+ linkAccount(account: AuthJsAccount): Promise<AuthJsAccount>;
63
+ unlinkAccount(account: Pick<AuthJsAccount, 'provider' | 'providerAccountId'>): Promise<void>;
64
+ getAccount(providerAccountId: string, provider: string): Promise<AuthJsAccount | null>;
65
+ createSession(session: AuthJsSession): Promise<AuthJsSession>;
66
+ getSessionAndUser(sessionToken: string): Promise<{
67
+ session: AuthJsSession;
68
+ user: AuthJsUser;
69
+ } | null>;
70
+ updateSession(session: Partial<AuthJsSession> & Pick<AuthJsSession, 'sessionToken'>): Promise<AuthJsSession | null>;
71
+ deleteSession(sessionToken: string): Promise<void>;
72
+ createVerificationToken(token: AuthJsVerificationToken): Promise<AuthJsVerificationToken>;
73
+ useVerificationToken(params: {
74
+ identifier: string;
75
+ token: string;
76
+ }): Promise<AuthJsVerificationToken | null>;
77
+ }
78
+ /** Where the adapter keeps its tables, and how it makes ids. */
79
+ export interface AuthJsAdapterOptions {
80
+ /** Put before each table name: `<prefix>users`, `<prefix>accounts`, `<prefix>sessions`, `<prefix>verification_tokens`. Default `auth_`. */
81
+ tablePrefix?: string;
82
+ /** Makes a user or account id when Auth.js passes none. Default: `crypto.randomUUID()`. */
83
+ generateId?: () => string;
84
+ }
85
+ /**
86
+ * The statements that create the adapter's tables and indexes on `db`'s
87
+ * engine, each `IF NOT EXISTS`, for a migration file or for
88
+ * ensureAuthJsTables.
89
+ */
90
+ export declare function authJsTablesSql(db: StorageAdapter, options?: AuthJsAdapterOptions): string[];
91
+ /** Creates the adapter's tables and indexes on `db` when they are missing. */
92
+ export declare function ensureAuthJsTables(db: StorageAdapter, options?: AuthJsAdapterOptions): Promise<void>;
93
+ /**
94
+ * An Auth.js database adapter that keeps users, accounts, sessions and
95
+ * verification tokens in `db`, on PostgreSQL or SQLite. Create the tables
96
+ * first with ensureAuthJsTables (or authJsTablesSql in a migration).
97
+ *
98
+ * @example
99
+ * ```typescript
100
+ * import NextAuth from 'next-auth';
101
+ * import { createDatabase } from '@framers/sql-storage-adapter';
102
+ * import { createAuthJsAdapter, ensureAuthJsTables } from '@framers/sql-storage-adapter/authjs';
103
+ *
104
+ * const db = await createDatabase({ url: process.env.DATABASE_URL, strict: true });
105
+ * await ensureAuthJsTables(db);
106
+ * export const { handlers, auth } = NextAuth({ adapter: createAuthJsAdapter(db), providers: [] });
107
+ * ```
108
+ */
109
+ export declare function createAuthJsAdapter(db: StorageAdapter, options?: AuthJsAdapterOptions): AuthJsAdapter;
110
+ //# sourceMappingURL=authJsAdapter.d.ts.map
@@ -0,0 +1,251 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __export = (target, all) => {
6
+ for (var name in all)
7
+ __defProp(target, name, { get: all[name], enumerable: true });
8
+ };
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
18
+ var authJsAdapter_exports = {};
19
+ __export(authJsAdapter_exports, {
20
+ authJsTablesSql: () => authJsTablesSql,
21
+ createAuthJsAdapter: () => createAuthJsAdapter,
22
+ ensureAuthJsTables: () => ensureAuthJsTables
23
+ });
24
+ module.exports = __toCommonJS(authJsAdapter_exports);
25
+ var import_features = require("../../core/contracts/features.js");
26
+ const PREFIX = /^[A-Za-z_][A-Za-z0-9_]*$/;
27
+ const tablesFor = (options) => {
28
+ const prefix = options.tablePrefix ?? "auth_";
29
+ if (!PREFIX.test(prefix)) {
30
+ throw new Error(`tablePrefix must be letters, digits and underscores, starting with a letter or an underscore; got ${JSON.stringify(prefix)}.`);
31
+ }
32
+ return {
33
+ users: `${prefix}users`,
34
+ accounts: `${prefix}accounts`,
35
+ sessions: `${prefix}sessions`,
36
+ verificationTokens: `${prefix}verification_tokens`
37
+ };
38
+ };
39
+ function authJsTablesSql(db, options = {}) {
40
+ const t = tablesFor(options);
41
+ const d = (0, import_features.createStorageFeatures)(db).dialect;
42
+ const timestamp = d.columnType("timestamp");
43
+ return [
44
+ `CREATE TABLE IF NOT EXISTS ${t.users} (id TEXT PRIMARY KEY, name TEXT, email TEXT UNIQUE, email_verified ${timestamp}, image TEXT)`,
45
+ `CREATE TABLE IF NOT EXISTS ${t.accounts} (
46
+ id TEXT PRIMARY KEY,
47
+ user_id TEXT NOT NULL REFERENCES ${t.users} (id) ON DELETE CASCADE,
48
+ type TEXT NOT NULL,
49
+ provider TEXT NOT NULL,
50
+ provider_account_id TEXT NOT NULL,
51
+ refresh_token TEXT,
52
+ access_token TEXT,
53
+ expires_at ${d.columnType("bigint")},
54
+ token_type TEXT,
55
+ scope TEXT,
56
+ id_token TEXT,
57
+ session_state TEXT,
58
+ UNIQUE (provider, provider_account_id)
59
+ )`,
60
+ `CREATE TABLE IF NOT EXISTS ${t.sessions} (session_token TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES ${t.users} (id) ON DELETE CASCADE, expires ${timestamp} NOT NULL)`,
61
+ `CREATE TABLE IF NOT EXISTS ${t.verificationTokens} (identifier TEXT NOT NULL, token TEXT NOT NULL, expires ${timestamp} NOT NULL, PRIMARY KEY (identifier, token))`,
62
+ `CREATE INDEX IF NOT EXISTS ${t.accounts}_user_id ON ${t.accounts} (user_id)`,
63
+ `CREATE INDEX IF NOT EXISTS ${t.sessions}_user_id ON ${t.sessions} (user_id)`
64
+ ];
65
+ }
66
+ async function ensureAuthJsTables(db, options = {}) {
67
+ for (const statement of authJsTablesSql(db, options)) {
68
+ await db.exec(statement);
69
+ }
70
+ }
71
+ const toDate = (value) => value == null ? null : value instanceof Date ? value : new Date(String(value));
72
+ const toUser = (row) => ({
73
+ id: String(row.id),
74
+ name: row.name ?? null,
75
+ email: row.email,
76
+ emailVerified: toDate(row.email_verified),
77
+ image: row.image ?? null
78
+ });
79
+ const toAccount = (row) => {
80
+ const account = {
81
+ userId: String(row.user_id),
82
+ type: row.type,
83
+ provider: row.provider,
84
+ providerAccountId: row.provider_account_id
85
+ };
86
+ for (const field of ["refresh_token", "access_token", "token_type", "scope", "id_token", "session_state"]) {
87
+ if (row[field] != null)
88
+ account[field] = String(row[field]);
89
+ }
90
+ if (row.expires_at != null)
91
+ account.expires_at = Number(row.expires_at);
92
+ return account;
93
+ };
94
+ const toSession = (row) => ({
95
+ sessionToken: row.session_token,
96
+ userId: String(row.user_id),
97
+ expires: toDate(row.expires)
98
+ });
99
+ const toVerificationToken = (row) => ({
100
+ identifier: row.identifier,
101
+ token: row.token,
102
+ expires: toDate(row.expires)
103
+ });
104
+ const defaultId = () => {
105
+ const random = globalThis.crypto?.randomUUID;
106
+ if (!random)
107
+ throw new Error("crypto.randomUUID() is not available; pass generateId.");
108
+ return random.call(globalThis.crypto);
109
+ };
110
+ function createAuthJsAdapter(db, options = {}) {
111
+ const t = tablesFor(options);
112
+ const generateId = options.generateId ?? defaultId;
113
+ const getUser = async (id) => {
114
+ const row = await db.get(`SELECT * FROM ${t.users} WHERE id = ?`, [id]);
115
+ return row ? toUser(row) : null;
116
+ };
117
+ const getSession = async (sessionToken) => {
118
+ const row = await db.get(`SELECT * FROM ${t.sessions} WHERE session_token = ?`, [sessionToken]);
119
+ return row ? toSession(row) : null;
120
+ };
121
+ return {
122
+ async createUser(user) {
123
+ const id = user.id ?? generateId();
124
+ await db.run(`INSERT INTO ${t.users} (id, name, email, email_verified, image) VALUES (?, ?, ?, ?, ?)`, [
125
+ id,
126
+ user.name ?? null,
127
+ user.email,
128
+ user.emailVerified ?? null,
129
+ user.image ?? null
130
+ ]);
131
+ return await getUser(id);
132
+ },
133
+ getUser,
134
+ async getUserByEmail(email) {
135
+ const row = await db.get(`SELECT * FROM ${t.users} WHERE email = ?`, [email]);
136
+ return row ? toUser(row) : null;
137
+ },
138
+ async getUserByAccount({ provider, providerAccountId }) {
139
+ const row = await db.get(`SELECT u.* FROM ${t.users} u JOIN ${t.accounts} a ON a.user_id = u.id WHERE a.provider = ? AND a.provider_account_id = ?`, [provider, providerAccountId]);
140
+ return row ? toUser(row) : null;
141
+ },
142
+ async updateUser(user) {
143
+ const fields = [];
144
+ if ("name" in user)
145
+ fields.push(["name", user.name ?? null]);
146
+ if ("email" in user)
147
+ fields.push(["email", user.email]);
148
+ if ("emailVerified" in user)
149
+ fields.push(["email_verified", user.emailVerified ?? null]);
150
+ if ("image" in user)
151
+ fields.push(["image", user.image ?? null]);
152
+ if (fields.length) {
153
+ await db.run(`UPDATE ${t.users} SET ${fields.map(([column]) => `${column} = ?`).join(", ")} WHERE id = ?`, [
154
+ ...fields.map(([, value]) => value),
155
+ user.id
156
+ ]);
157
+ }
158
+ const updated = await getUser(user.id);
159
+ if (!updated)
160
+ throw new Error(`No user with id ${JSON.stringify(user.id)} to update.`);
161
+ return updated;
162
+ },
163
+ async deleteUser(userId) {
164
+ return db.transaction(async (trx) => {
165
+ const row = await trx.get(`SELECT * FROM ${t.users} WHERE id = ?`, [userId]);
166
+ await trx.run(`DELETE FROM ${t.sessions} WHERE user_id = ?`, [userId]);
167
+ await trx.run(`DELETE FROM ${t.accounts} WHERE user_id = ?`, [userId]);
168
+ await trx.run(`DELETE FROM ${t.users} WHERE id = ?`, [userId]);
169
+ return row ? toUser(row) : null;
170
+ });
171
+ },
172
+ async linkAccount(account) {
173
+ await db.run(`INSERT INTO ${t.accounts} (id, user_id, type, provider, provider_account_id, refresh_token, access_token, expires_at, token_type, scope, id_token, session_state)
174
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
175
+ generateId(),
176
+ account.userId,
177
+ account.type,
178
+ account.provider,
179
+ account.providerAccountId,
180
+ account.refresh_token ?? null,
181
+ account.access_token ?? null,
182
+ account.expires_at ?? null,
183
+ account.token_type ?? null,
184
+ account.scope ?? null,
185
+ account.id_token ?? null,
186
+ account.session_state ?? null
187
+ ]);
188
+ return account;
189
+ },
190
+ async unlinkAccount({ provider, providerAccountId }) {
191
+ await db.run(`DELETE FROM ${t.accounts} WHERE provider = ? AND provider_account_id = ?`, [provider, providerAccountId]);
192
+ },
193
+ async getAccount(providerAccountId, provider) {
194
+ const row = await db.get(`SELECT * FROM ${t.accounts} WHERE provider = ? AND provider_account_id = ?`, [
195
+ provider,
196
+ providerAccountId
197
+ ]);
198
+ return row ? toAccount(row) : null;
199
+ },
200
+ async createSession(session) {
201
+ await db.run(`INSERT INTO ${t.sessions} (session_token, user_id, expires) VALUES (?, ?, ?)`, [
202
+ session.sessionToken,
203
+ session.userId,
204
+ session.expires
205
+ ]);
206
+ return { sessionToken: session.sessionToken, userId: session.userId, expires: session.expires };
207
+ },
208
+ async getSessionAndUser(sessionToken) {
209
+ const session = await getSession(sessionToken);
210
+ if (!session)
211
+ return null;
212
+ const user = await getUser(session.userId);
213
+ return user ? { session, user } : null;
214
+ },
215
+ async updateSession(session) {
216
+ const fields = [];
217
+ if (session.expires !== void 0)
218
+ fields.push(["expires", session.expires]);
219
+ if (session.userId !== void 0)
220
+ fields.push(["user_id", session.userId]);
221
+ if (fields.length) {
222
+ await db.run(`UPDATE ${t.sessions} SET ${fields.map(([column]) => `${column} = ?`).join(", ")} WHERE session_token = ?`, [
223
+ ...fields.map(([, value]) => value),
224
+ session.sessionToken
225
+ ]);
226
+ }
227
+ return getSession(session.sessionToken);
228
+ },
229
+ async deleteSession(sessionToken) {
230
+ await db.run(`DELETE FROM ${t.sessions} WHERE session_token = ?`, [sessionToken]);
231
+ },
232
+ async createVerificationToken(token) {
233
+ await db.run(`INSERT INTO ${t.verificationTokens} (identifier, token, expires) VALUES (?, ?, ?)`, [
234
+ token.identifier,
235
+ token.token,
236
+ token.expires
237
+ ]);
238
+ return { identifier: token.identifier, token: token.token, expires: token.expires };
239
+ },
240
+ async useVerificationToken({ identifier, token }) {
241
+ const row = await db.get(`DELETE FROM ${t.verificationTokens} WHERE identifier = ? AND token = ? RETURNING identifier, token, expires`, [identifier, token]);
242
+ return row ? toVerificationToken(row) : null;
243
+ }
244
+ };
245
+ }
246
+ // Annotate the CommonJS export names for ESM import in node:
247
+ 0 && (module.exports = {
248
+ authJsTablesSql,
249
+ createAuthJsAdapter,
250
+ ensureAuthJsTables
251
+ });
@@ -1,29 +1,30 @@
1
1
  import type { IFullTextSearch } from '../core/contracts/fts.js';
2
+ type Weight = 'A' | 'B' | 'C' | 'D';
2
3
  /**
3
4
  * Postgres full-text search using tsvector columns and GIN indexes.
4
5
  *
5
- * Unlike FTS5 (separate virtual table), Postgres stores the tsvector
6
- * as a column `_tsv` on the content table itself. The `createIndex()`
7
- * call adds the column, creates the GIN index, and backfills existing rows.
6
+ * Unlike FTS5 (separate virtual table), Postgres stores the tsvector as a
7
+ * column `_tsv` on the content table itself. The `createIndex()` call adds the
8
+ * column, creates the GIN index, and backfills existing rows. Queries go
9
+ * through `websearch_to_tsquery`, which reads raw user text without syntax
10
+ * errors: words are ANDed, and `or`, `-word` and quoted phrases work as in a
11
+ * web search box.
8
12
  */
9
13
  export declare class PostgresFts implements IFullTextSearch {
10
- /** The content table is stored during createIndex for use by other methods. */
11
- private _contentTable;
12
- private _columns;
13
- private _lang;
14
14
  createIndex(config: {
15
15
  table: string;
16
16
  columns: string[];
17
17
  contentTable?: string;
18
18
  tokenizer?: string;
19
+ weights?: Record<string, Weight>;
19
20
  }): string;
20
- matchClause(_indexName: string, queryPlaceholder: string): string;
21
- rankExpression(_indexName: string, queryPlaceholder?: string): string;
22
- rebuildCommand(_indexName: string): string;
23
- syncInsert(_indexName: string, _rowIdExpr: string, columns: string[]): string;
21
+ matchClause(indexName: string, queryPlaceholder: string): string;
22
+ rankExpression(indexName: string, queryPlaceholder?: string): string;
23
+ relevanceOrder(indexName: string, queryPlaceholder?: string): string;
24
+ rebuildCommand(indexName: string): string;
25
+ syncInsert(indexName: string, _rowIdExpr: string, columns: string[]): string;
24
26
  sanitizeQuery(input: string): string;
25
27
  joinClause(contentTable: string, contentAlias: string, _ftsAlias: string, _indexName: string): string;
26
- /** Map FTS5-style tokenizer strings to Postgres text search configurations. */
27
- private _tokenizerToLang;
28
28
  }
29
+ export {};
29
30
  //# sourceMappingURL=PostgresFts.d.ts.map