@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.
- package/README.md +45 -13
- package/dist/cjs/core/contracts/features.js +4 -2
- package/dist/cjs/core/contracts/fts.d.ts +20 -8
- package/dist/cjs/core/portable.d.ts +2 -0
- package/dist/cjs/core/portable.js +5 -0
- package/dist/cjs/features/auth/authJsAdapter.d.ts +110 -0
- package/dist/cjs/features/auth/authJsAdapter.js +251 -0
- package/dist/cjs/fts/PostgresFts.d.ts +14 -13
- package/dist/cjs/fts/PostgresFts.js +61 -33
- package/dist/cjs/fts/SqliteFts4.d.ts +28 -0
- package/dist/cjs/fts/SqliteFts4.js +67 -0
- package/dist/cjs/fts/SqliteFts5.d.ts +1 -0
- package/dist/cjs/fts/SqliteFts5.js +3 -0
- package/dist/cjs/index.d.ts +2 -0
- package/dist/cjs/index.js +5 -0
- package/dist/core/contracts/features.d.ts.map +1 -1
- package/dist/core/contracts/features.js +6 -2
- package/dist/core/contracts/features.js.map +1 -1
- package/dist/core/contracts/fts.d.ts +20 -8
- package/dist/core/contracts/fts.d.ts.map +1 -1
- package/dist/core/portable.d.ts +2 -0
- package/dist/core/portable.d.ts.map +1 -1
- package/dist/core/portable.js +2 -0
- package/dist/core/portable.js.map +1 -1
- package/dist/features/auth/authJsAdapter.d.ts +110 -0
- package/dist/features/auth/authJsAdapter.d.ts.map +1 -0
- package/dist/features/auth/authJsAdapter.js +248 -0
- package/dist/features/auth/authJsAdapter.js.map +1 -0
- package/dist/fts/PostgresFts.d.ts +14 -13
- package/dist/fts/PostgresFts.d.ts.map +1 -1
- package/dist/fts/PostgresFts.js +83 -39
- package/dist/fts/PostgresFts.js.map +1 -1
- package/dist/fts/SqliteFts4.d.ts +28 -0
- package/dist/fts/SqliteFts4.d.ts.map +1 -0
- package/dist/fts/SqliteFts4.js +56 -0
- package/dist/fts/SqliteFts4.js.map +1 -0
- package/dist/fts/SqliteFts5.d.ts +1 -0
- package/dist/fts/SqliteFts5.d.ts.map +1 -1
- package/dist/fts/SqliteFts5.js +3 -0
- package/dist/fts/SqliteFts5.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- 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
|
-
//
|
|
718
|
-
await
|
|
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
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
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
|
-
*
|
|
5
|
-
*
|
|
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: `
|
|
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
|
-
*
|
|
31
|
-
* SQLite: `memory_traces_fts.rank`
|
|
32
|
-
*
|
|
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
|
-
*
|
|
50
|
-
*
|
|
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
|
-
*
|
|
7
|
-
*
|
|
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(
|
|
21
|
-
rankExpression(
|
|
22
|
-
|
|
23
|
-
|
|
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
|