terrascale 0.3.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/LICENSE +202 -0
- package/README.md +143 -0
- package/package.json +159 -0
- package/sdk-current-contract.json +27 -0
- package/sdk-route-manifest.json +67 -0
- package/src/admin.js +9 -0
- package/src/better-auth.js +14 -0
- package/src/config.js +121 -0
- package/src/database-codec.js +845 -0
- package/src/database-types.js +237 -0
- package/src/database-view.js +422 -0
- package/src/database.js +420 -0
- package/src/discovery.js +374 -0
- package/src/http.js +887 -0
- package/src/index.js +79 -0
- package/src/local/authentication.js +47 -0
- package/src/local/better-auth.js +517 -0
- package/src/local/cli.js +51 -0
- package/src/local/context.js +23 -0
- package/src/local/environment.js +109 -0
- package/src/local/index.js +204 -0
- package/src/local/router.js +999 -0
- package/src/local/server.js +664 -0
- package/src/local/store.js +530 -0
- package/src/local/test-environment.js +74 -0
- package/src/management-contracts.js +72 -0
- package/src/management.js +12 -0
- package/src/native-origin.js +75 -0
- package/src/postgres.js +494 -0
- package/src/react/core.js +743 -0
- package/src/react/index.js +99 -0
- package/src/result.js +251 -0
- package/src/schema.js +366 -0
- package/src/sql.js +996 -0
- package/src/svelte/index.js +129 -0
- package/src/tanstack/index.js +511 -0
- package/src/ts-auth-discovery.js +190 -0
- package/src/ts-auth.js +3497 -0
- package/types/admin.d.ts +6 -0
- package/types/better-auth.d.ts +8 -0
- package/types/config.d.ts +58 -0
- package/types/database-codec.d.ts +111 -0
- package/types/database-types.d.ts +213 -0
- package/types/database-view.d.ts +183 -0
- package/types/database.d.ts +98 -0
- package/types/discovery.d.ts +114 -0
- package/types/http.d.ts +46 -0
- package/types/index.d.ts +52 -0
- package/types/local/authentication.d.ts +11 -0
- package/types/local/better-auth.d.ts +33 -0
- package/types/local/cli.d.ts +2 -0
- package/types/local/context.d.ts +14 -0
- package/types/local/environment.d.ts +23 -0
- package/types/local/index.d.ts +94 -0
- package/types/local/router.d.ts +66 -0
- package/types/local/server.d.ts +54 -0
- package/types/local/store.d.ts +106 -0
- package/types/local/test-environment.d.ts +25 -0
- package/types/management-contracts.d.ts +44 -0
- package/types/management.d.ts +6 -0
- package/types/native-origin.d.ts +23 -0
- package/types/postgres.d.ts +123 -0
- package/types/react/core.d.ts +366 -0
- package/types/react/index.d.ts +54 -0
- package/types/result.d.ts +161 -0
- package/types/schema.d.ts +145 -0
- package/types/sql.d.ts +288 -0
- package/types/svelte/index.d.ts +81 -0
- package/types/tanstack/index.d.ts +165 -0
- package/types/ts-auth-discovery.d.ts +11 -0
- package/types/ts-auth.d.ts +1826 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export { createTerraBaseClient, TerraBaseClient } from "./database.js";
|
|
2
|
+
export {
|
|
3
|
+
terraBaseDiscoveryOrigin,
|
|
4
|
+
parseTerraBaseRoutingDocument,
|
|
5
|
+
TerraBaseDiscoveryError,
|
|
6
|
+
TerraBaseDiscoveryRouter,
|
|
7
|
+
} from "./discovery.js";
|
|
8
|
+
export * from "./schema.js";
|
|
9
|
+
export * from "./sql.js";
|
|
10
|
+
export {
|
|
11
|
+
terraScaleReleaseBlockers,
|
|
12
|
+
TerraScaleCapabilityUnavailableError,
|
|
13
|
+
} from "./management-contracts.js";
|
|
14
|
+
export {
|
|
15
|
+
createDatabaseQuery,
|
|
16
|
+
DatabaseSubscriptionView,
|
|
17
|
+
DatabaseSubscriptionError,
|
|
18
|
+
} from "./database-view.js";
|
|
19
|
+
|
|
20
|
+
/** @typedef {import("./database.js").TerraBaseClientConfig} TerraBaseClientConfig */
|
|
21
|
+
/** @typedef {import("./database.js").PreparedDatabaseCommit} PreparedDatabaseCommit */
|
|
22
|
+
/** @typedef {import("./database.js").DatabaseRequestOptions} DatabaseRequestOptions */
|
|
23
|
+
|
|
24
|
+
/** @typedef {import("./database-types.js").CanonicalUuid} CanonicalUuid */
|
|
25
|
+
/** @typedef {import("./database-types.js").DatabaseScope} DatabaseScope */
|
|
26
|
+
/** @typedef {import("./database-types.js").CollectionBinding} CollectionBinding */
|
|
27
|
+
/** @typedef {import("./database-types.js").DatabaseValue} DatabaseValue */
|
|
28
|
+
/** @typedef {import("./database-types.js").DatabaseField} DatabaseField */
|
|
29
|
+
/** @typedef {import("./database-types.js").DatabaseObserved} DatabaseObserved */
|
|
30
|
+
/** @typedef {import("./database-types.js").DatabaseEvent} DatabaseEvent */
|
|
31
|
+
/** @typedef {import("./database-types.js").DatabaseAuthority} DatabaseAuthority */
|
|
32
|
+
/** @typedef {import("./database-types.js").DatabaseCondition} DatabaseCondition */
|
|
33
|
+
/** @typedef {import("./database-types.js").DatabaseWriteValue} DatabaseWriteValue */
|
|
34
|
+
/** @typedef {import("./database-types.js").DatabaseWrite} DatabaseWrite */
|
|
35
|
+
/** @typedef {import("./database-types.js").DatabaseTransactionId} DatabaseTransactionId */
|
|
36
|
+
/** @typedef {import("./database-types.js").DatabaseCommit} DatabaseCommit */
|
|
37
|
+
/** @typedef {import("./database-types.js").DatabaseSibling} DatabaseSibling */
|
|
38
|
+
/** @typedef {import("./database-types.js").DatabasePoint} DatabasePoint */
|
|
39
|
+
/** @typedef {import("./database-types.js").DatabaseQueryValue} DatabaseQueryValue */
|
|
40
|
+
/** @typedef {import("./database-types.js").DatabaseQueryPredicate} DatabaseQueryPredicate */
|
|
41
|
+
/** @typedef {import("./database-types.js").DatabaseQuery} DatabaseQuery */
|
|
42
|
+
/** @typedef {import("./database-types.js").DatabaseQueryRow} DatabaseQueryRow */
|
|
43
|
+
/** @typedef {import("./database-types.js").DatabaseQueryPage} DatabaseQueryPage */
|
|
44
|
+
/** @typedef {import("./database-types.js").DatabaseCommitReceipt} DatabaseCommitReceipt */
|
|
45
|
+
/** @typedef {import("./database-types.js").DatabaseTransactionIdentity} DatabaseTransactionIdentity */
|
|
46
|
+
/** @typedef {import("./database-types.js").DatabaseTransactionStatus} DatabaseTransactionStatus */
|
|
47
|
+
/**
|
|
48
|
+
* @template T
|
|
49
|
+
* @typedef {import("./database-types.js").DatabaseResult<T>} DatabaseResult
|
|
50
|
+
*/
|
|
51
|
+
/** @typedef {import("./database-types.js").DatabaseSubscriptionRequest} DatabaseSubscriptionRequest */
|
|
52
|
+
/** @typedef {import("./database-types.js").DatabaseSubscriptionEffect} DatabaseSubscriptionEffect */
|
|
53
|
+
/** @typedef {import("./database-types.js").DatabaseSubscriptionGroup} DatabaseSubscriptionGroup */
|
|
54
|
+
/** @typedef {import("./database-types.js").DatabaseSubscriptionResponse} DatabaseSubscriptionResponse */
|
|
55
|
+
|
|
56
|
+
/** @typedef {import("./discovery.js").TerraBaseRoutingCapability} TerraBaseRoutingCapability */
|
|
57
|
+
/** @typedef {import("./discovery.js").TerraBaseRoutingCapabilities} TerraBaseRoutingCapabilities */
|
|
58
|
+
/** @typedef {import("./discovery.js").TerraBaseRoutingEndpoint} TerraBaseRoutingEndpoint */
|
|
59
|
+
/** @typedef {import("./discovery.js").TerraBaseRoutingDocument} TerraBaseRoutingDocument */
|
|
60
|
+
|
|
61
|
+
/** @typedef {import("./management-contracts.js").TerraScaleBlockedCapability} TerraScaleBlockedCapability */
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @template TValue
|
|
65
|
+
* @typedef {import("./result.js").TerraScaleResult<TValue>} TerraScaleResult
|
|
66
|
+
*/
|
|
67
|
+
/** @typedef {import("./result.js").TerraScaleError} TerraScaleError */
|
|
68
|
+
/** @typedef {import("./result.js").TerraScaleFailure} TerraScaleFailure */
|
|
69
|
+
/**
|
|
70
|
+
* @template TValue
|
|
71
|
+
* @typedef {import("./result.js").TerraScaleSuccess<TValue>} TerraScaleSuccess
|
|
72
|
+
*/
|
|
73
|
+
|
|
74
|
+
/** @typedef {import("./database-view.js").DatabaseQueryOptions} DatabaseQueryOptions */
|
|
75
|
+
/** @typedef {import("./database-view.js").DatabaseViewBinding} DatabaseViewBinding */
|
|
76
|
+
/** @typedef {import("./database-view.js").DatabaseViewPosition} DatabaseViewPosition */
|
|
77
|
+
/** @typedef {import("./database-view.js").DatabaseViewSnapshot} DatabaseViewSnapshot */
|
|
78
|
+
/** @typedef {import("./database-view.js").DatabaseQueryCallbacks} DatabaseQueryCallbacks */
|
|
79
|
+
/** @typedef {import("./database-view.js").DatabaseQuerySource} DatabaseQuerySource */
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createHash, timingSafeEqual } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
const MINIMUM_BEARER_TOKEN_LENGTH = 32;
|
|
4
|
+
const MAXIMUM_BEARER_TOKEN_LENGTH = 512;
|
|
5
|
+
const BEARER_TOKEN_PATTERN = /^[A-Za-z0-9._~+\/-]+=*$/u;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {unknown} value
|
|
9
|
+
* @returns {string}
|
|
10
|
+
*/
|
|
11
|
+
export function normalizeLocalBearerToken(value) {
|
|
12
|
+
if (typeof value !== "string") {
|
|
13
|
+
throw new TypeError('bearerToken is required when authentication is "required".');
|
|
14
|
+
}
|
|
15
|
+
if (
|
|
16
|
+
value.length < MINIMUM_BEARER_TOKEN_LENGTH
|
|
17
|
+
|| value.length > MAXIMUM_BEARER_TOKEN_LENGTH
|
|
18
|
+
|| !BEARER_TOKEN_PATTERN.test(value)
|
|
19
|
+
) {
|
|
20
|
+
throw new TypeError(
|
|
21
|
+
`bearerToken must contain between ${MINIMUM_BEARER_TOKEN_LENGTH} and ${MAXIMUM_BEARER_TOKEN_LENGTH} ASCII bearer-token characters.`,
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {string | null} authorization
|
|
29
|
+
* @param {string} expectedToken
|
|
30
|
+
* @returns {boolean}
|
|
31
|
+
*/
|
|
32
|
+
export function matchesLocalBearerAuthorization(authorization, expectedToken) {
|
|
33
|
+
if (authorization === null) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
const match = /^Bearer ([A-Za-z0-9._~+\/-]+=*)$/iu.exec(authorization.trim());
|
|
37
|
+
if (match === null) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
const candidateToken = match[1];
|
|
41
|
+
if (candidateToken === undefined) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
const expectedDigest = createHash("sha256").update(expectedToken).digest();
|
|
45
|
+
const candidateDigest = createHash("sha256").update(candidateToken).digest();
|
|
46
|
+
return timingSafeEqual(expectedDigest, candidateDigest);
|
|
47
|
+
}
|
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { createAdapterFactory } from "@better-auth/core/db/adapter";
|
|
3
|
+
/** @import { CleanedWhere, CustomAdapter, DBAdapter, JoinConfig } from "@better-auth/core/db/adapter" */
|
|
4
|
+
/** @import { BetterAuthOptions } from "@better-auth/core" */
|
|
5
|
+
/** @import { TerraScaleSqliteStore } from "./store.js" */
|
|
6
|
+
|
|
7
|
+
/** @typedef {Record<string, unknown>} Row */
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {Parameters<
|
|
10
|
+
* NonNullable<Parameters<typeof createAdapterFactory>[0]["adapter"]>
|
|
11
|
+
* >[0]} FactoryConfig
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {(
|
|
15
|
+
* options: BetterAuthOptions,
|
|
16
|
+
* ) => DBAdapter<BetterAuthOptions>} LocalBetterAuthAdapterFactory
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Better Auth 1.7.2 over explicitly enabled development SQLite only.
|
|
21
|
+
*
|
|
22
|
+
* @param {TerraScaleSqliteStore} store
|
|
23
|
+
* @returns {LocalBetterAuthAdapterFactory}
|
|
24
|
+
*/
|
|
25
|
+
export function createLocalBetterAuthAdapter(store) {
|
|
26
|
+
/**
|
|
27
|
+
* @param {BetterAuthOptions} options
|
|
28
|
+
* @param {boolean} scoped
|
|
29
|
+
* @returns {DBAdapter<BetterAuthOptions>}
|
|
30
|
+
*/
|
|
31
|
+
function make(options, scoped) {
|
|
32
|
+
/** @type {ReturnType<typeof createAdapterFactory<BetterAuthOptions>>} */
|
|
33
|
+
const factory = createAdapterFactory({
|
|
34
|
+
config: {
|
|
35
|
+
adapterId: "terrascale-local-sqlite",
|
|
36
|
+
supportsDates: false,
|
|
37
|
+
supportsJSON: true,
|
|
38
|
+
supportsArrays: true,
|
|
39
|
+
supportsBooleans: true,
|
|
40
|
+
supportsNumericIds: false,
|
|
41
|
+
supportsUUIDs: true,
|
|
42
|
+
transaction: async (callback) => {
|
|
43
|
+
if (scoped)
|
|
44
|
+
throw new Error("Nested local auth transactions are unavailable.");
|
|
45
|
+
return store.transactionAsync(async () =>
|
|
46
|
+
serializedCallback(make(options, true), callback),
|
|
47
|
+
);
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
adapter: (config) => customAdapter(store, config, scoped),
|
|
51
|
+
});
|
|
52
|
+
return factory(options);
|
|
53
|
+
}
|
|
54
|
+
return (options) => make(options, false);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @param {TerraScaleSqliteStore} store
|
|
59
|
+
* @param {FactoryConfig} config
|
|
60
|
+
* @param {boolean} scoped
|
|
61
|
+
* @returns {CustomAdapter}
|
|
62
|
+
*/
|
|
63
|
+
function customAdapter(store, config, scoped) {
|
|
64
|
+
/**
|
|
65
|
+
* @param {string} model
|
|
66
|
+
* @returns {Row[]}
|
|
67
|
+
*/
|
|
68
|
+
const rows = (model) => store.list("local-auth", model);
|
|
69
|
+
/**
|
|
70
|
+
* @param {string} model
|
|
71
|
+
* @param {readonly CleanedWhere[]} clauses
|
|
72
|
+
* @returns {readonly CleanedWhere[]}
|
|
73
|
+
*/
|
|
74
|
+
const where = (model, clauses) =>
|
|
75
|
+
clauses.map((clause) => ({
|
|
76
|
+
...clause,
|
|
77
|
+
field: config.getFieldName({ model, field: clause.field }),
|
|
78
|
+
}));
|
|
79
|
+
/**
|
|
80
|
+
* @param {string} model
|
|
81
|
+
* @param {readonly CleanedWhere[]} [clauses]
|
|
82
|
+
* @returns {Row[]}
|
|
83
|
+
*/
|
|
84
|
+
const find = (model, clauses = []) =>
|
|
85
|
+
rows(model).filter((row) => matches(row, where(model, clauses)));
|
|
86
|
+
/**
|
|
87
|
+
* @param {string} model
|
|
88
|
+
* @returns {readonly (readonly string[])[]}
|
|
89
|
+
*/
|
|
90
|
+
const indexes = (model) =>
|
|
91
|
+
/** @type {readonly (readonly string[])[] | undefined} */ (
|
|
92
|
+
store.getMeta(`local-auth-indexes:${model}`)
|
|
93
|
+
) ?? [["id"]];
|
|
94
|
+
/** @returns {void} */
|
|
95
|
+
const installSchema = () => {
|
|
96
|
+
for (const [model, table] of Object.entries(config.schema)) {
|
|
97
|
+
const fields = [
|
|
98
|
+
["id"],
|
|
99
|
+
...Object.entries(table.fields)
|
|
100
|
+
.filter(([, field]) => field.unique === true)
|
|
101
|
+
.map(([field]) => [config.getFieldName({ model, field })]),
|
|
102
|
+
...(table.indexes ?? [])
|
|
103
|
+
.filter((index) => index.unique === true)
|
|
104
|
+
.map((index) =>
|
|
105
|
+
index.fields.map((field) => config.getFieldName({ model, field })),
|
|
106
|
+
),
|
|
107
|
+
];
|
|
108
|
+
const name = config.getModelName(model);
|
|
109
|
+
const prior = indexes(name);
|
|
110
|
+
const merged = [...prior];
|
|
111
|
+
for (const index of fields)
|
|
112
|
+
if (
|
|
113
|
+
!merged.some(
|
|
114
|
+
(value) => JSON.stringify(value) === JSON.stringify(index),
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
merged.push(index);
|
|
118
|
+
for (const row of rows(name)) ensureUnique(name, row, merged);
|
|
119
|
+
store.setMeta(`local-auth-indexes:${name}`, merged);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* @param {string} model
|
|
124
|
+
* @param {Row} candidate
|
|
125
|
+
* @param {readonly (readonly string[])[]} [constraints]
|
|
126
|
+
* @returns {void}
|
|
127
|
+
*/
|
|
128
|
+
const ensureUnique = (model, candidate, constraints = indexes(model)) => {
|
|
129
|
+
for (const other of rows(model)) {
|
|
130
|
+
if (other.id === candidate.id) continue;
|
|
131
|
+
for (const fields of constraints) {
|
|
132
|
+
if (
|
|
133
|
+
fields.every(
|
|
134
|
+
(field) =>
|
|
135
|
+
candidate[field] !== null &&
|
|
136
|
+
candidate[field] !== undefined &&
|
|
137
|
+
equal(candidate[field], other[field]),
|
|
138
|
+
)
|
|
139
|
+
) {
|
|
140
|
+
throw new LocalAuthConstraintError();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
/**
|
|
146
|
+
* @param {string} model
|
|
147
|
+
* @param {Row} input
|
|
148
|
+
* @param {boolean} [creating]
|
|
149
|
+
* @returns {Row}
|
|
150
|
+
*/
|
|
151
|
+
const save = (model, input, creating = false) => {
|
|
152
|
+
const row = jsonRow(input);
|
|
153
|
+
const id =
|
|
154
|
+
typeof row.id === "string" && row.id.length > 0 ? row.id : randomUUID();
|
|
155
|
+
const candidate = { ...row, id };
|
|
156
|
+
if (creating && store.get("local-auth", model, id) !== undefined)
|
|
157
|
+
throw new LocalAuthConstraintError();
|
|
158
|
+
ensureUnique(model, candidate);
|
|
159
|
+
store.put("local-auth", model, id, candidate);
|
|
160
|
+
return candidate;
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* @template T
|
|
164
|
+
* @param {() => T} work
|
|
165
|
+
* @returns {Promise<T>}
|
|
166
|
+
*/
|
|
167
|
+
const run = async (work) => {
|
|
168
|
+
const operation = () => {
|
|
169
|
+
installSchema();
|
|
170
|
+
return work();
|
|
171
|
+
};
|
|
172
|
+
return scoped
|
|
173
|
+
? operation()
|
|
174
|
+
: store.exclusive(() => store.transaction(operation));
|
|
175
|
+
};
|
|
176
|
+
/**
|
|
177
|
+
* @param {string} model
|
|
178
|
+
* @param {readonly Row[]} selected
|
|
179
|
+
* @param {readonly string[] | undefined} select
|
|
180
|
+
* @param {JoinConfig | undefined} join
|
|
181
|
+
* @returns {Row[]}
|
|
182
|
+
*/
|
|
183
|
+
const page = (model, selected, select, join) =>
|
|
184
|
+
selected.map((row) => {
|
|
185
|
+
const result =
|
|
186
|
+
select === undefined || select.length === 0
|
|
187
|
+
? { ...row }
|
|
188
|
+
: Object.fromEntries(
|
|
189
|
+
select
|
|
190
|
+
.map((field) => config.getFieldName({ model, field }))
|
|
191
|
+
.filter((field) => Object.hasOwn(row, field))
|
|
192
|
+
.map((field) => [field, row[field]]),
|
|
193
|
+
);
|
|
194
|
+
for (const [output, relation] of Object.entries(join ?? {})) {
|
|
195
|
+
const related = rows(config.getModelName(output)).filter((candidate) =>
|
|
196
|
+
equal(candidate[relation.on.to], row[relation.on.from]),
|
|
197
|
+
);
|
|
198
|
+
result[output] =
|
|
199
|
+
relation.relation === "one-to-one"
|
|
200
|
+
? (related[0] ?? null)
|
|
201
|
+
: related.slice(0, bounded(relation.limit ?? 100, 1000));
|
|
202
|
+
}
|
|
203
|
+
return result;
|
|
204
|
+
});
|
|
205
|
+
return {
|
|
206
|
+
/**
|
|
207
|
+
* @template {Record<string, any>} T
|
|
208
|
+
* @param {Parameters<CustomAdapter["create"]>[0] & { data: T }} input
|
|
209
|
+
*/
|
|
210
|
+
create: async (input) =>
|
|
211
|
+
run(() => /** @type {T} */ (save(input.model, input.data, true))),
|
|
212
|
+
/**
|
|
213
|
+
* @template T
|
|
214
|
+
* @param {Parameters<CustomAdapter["update"]>[0] & { update: T }} input
|
|
215
|
+
*/
|
|
216
|
+
update: async (input) =>
|
|
217
|
+
/** @type {Promise<T | null>} */ (
|
|
218
|
+
run(() => {
|
|
219
|
+
const row = find(input.model, input.where)[0];
|
|
220
|
+
return row === undefined
|
|
221
|
+
? null
|
|
222
|
+
: save(input.model, { ...row, ...jsonRow(input.update), id: row.id });
|
|
223
|
+
})
|
|
224
|
+
),
|
|
225
|
+
updateMany: async (input) =>
|
|
226
|
+
run(() => {
|
|
227
|
+
const selected = find(input.model, input.where);
|
|
228
|
+
for (const row of selected)
|
|
229
|
+
save(input.model, { ...row, ...input.update, id: row.id });
|
|
230
|
+
return selected.length;
|
|
231
|
+
}),
|
|
232
|
+
/**
|
|
233
|
+
* @template T
|
|
234
|
+
* @param {Parameters<CustomAdapter["findOne"]>[0]} input
|
|
235
|
+
*/
|
|
236
|
+
findOne: async (input) =>
|
|
237
|
+
/** @type {Promise<T | null>} */ (
|
|
238
|
+
run(
|
|
239
|
+
() =>
|
|
240
|
+
page(
|
|
241
|
+
input.model,
|
|
242
|
+
find(input.model, input.where).slice(0, 1),
|
|
243
|
+
input.select,
|
|
244
|
+
input.join,
|
|
245
|
+
)[0] ?? null,
|
|
246
|
+
)
|
|
247
|
+
),
|
|
248
|
+
/**
|
|
249
|
+
* @template T
|
|
250
|
+
* @param {Parameters<CustomAdapter["findMany"]>[0]} input
|
|
251
|
+
*/
|
|
252
|
+
findMany: async (input) =>
|
|
253
|
+
/** @type {Promise<T[]>} */ (run(() => {
|
|
254
|
+
let selected = find(input.model, input.where);
|
|
255
|
+
if (input.sortBy !== undefined) {
|
|
256
|
+
const field = config.getFieldName({
|
|
257
|
+
model: input.model,
|
|
258
|
+
field: input.sortBy.field,
|
|
259
|
+
});
|
|
260
|
+
const direction = input.sortBy.direction === "desc" ? -1 : 1;
|
|
261
|
+
selected = selected.sort(
|
|
262
|
+
(left, right) =>
|
|
263
|
+
compare(left[field], right[field]) * direction ||
|
|
264
|
+
String(left.id).localeCompare(String(right.id)),
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
const offset = bounded(input.offset ?? 0, 1_000_000);
|
|
268
|
+
return page(
|
|
269
|
+
input.model,
|
|
270
|
+
selected.slice(offset, offset + bounded(input.limit, 1000)),
|
|
271
|
+
input.select,
|
|
272
|
+
input.join,
|
|
273
|
+
);
|
|
274
|
+
})),
|
|
275
|
+
delete: async (input) =>
|
|
276
|
+
run(() => {
|
|
277
|
+
const row = find(input.model, input.where)[0];
|
|
278
|
+
if (row !== undefined)
|
|
279
|
+
store.delete("local-auth", input.model, String(row.id));
|
|
280
|
+
}),
|
|
281
|
+
deleteMany: async (input) =>
|
|
282
|
+
run(() => {
|
|
283
|
+
const selected = find(input.model, input.where);
|
|
284
|
+
for (const row of selected)
|
|
285
|
+
store.delete("local-auth", input.model, String(row.id));
|
|
286
|
+
return selected.length;
|
|
287
|
+
}),
|
|
288
|
+
/**
|
|
289
|
+
* @template T
|
|
290
|
+
* @param {Parameters<CustomAdapter["consumeOne"]>[0]} input
|
|
291
|
+
*/
|
|
292
|
+
consumeOne: async (input) =>
|
|
293
|
+
/** @type {Promise<T | null>} */ (
|
|
294
|
+
run(() => {
|
|
295
|
+
const row = find(input.model, input.where)[0];
|
|
296
|
+
if (row !== undefined)
|
|
297
|
+
store.delete("local-auth", input.model, String(row.id));
|
|
298
|
+
return row ?? null;
|
|
299
|
+
})
|
|
300
|
+
),
|
|
301
|
+
/**
|
|
302
|
+
* @template T
|
|
303
|
+
* @param {Parameters<CustomAdapter["incrementOne"]>[0]} input
|
|
304
|
+
*/
|
|
305
|
+
incrementOne: async (input) =>
|
|
306
|
+
/** @type {Promise<T | null>} */ (run(() => {
|
|
307
|
+
const row = find(input.model, input.where)[0];
|
|
308
|
+
if (row === undefined) return null;
|
|
309
|
+
/** @type {Row} */
|
|
310
|
+
const updated = { ...row, ...input.set, id: row.id };
|
|
311
|
+
for (const [field, delta] of Object.entries(input.increment)) {
|
|
312
|
+
const name = config.getFieldName({ model: input.model, field });
|
|
313
|
+
const old = row[name] ?? 0;
|
|
314
|
+
if (
|
|
315
|
+
typeof old !== "number" ||
|
|
316
|
+
!Number.isSafeInteger(old) ||
|
|
317
|
+
!Number.isSafeInteger(delta) ||
|
|
318
|
+
!Number.isSafeInteger(old + delta)
|
|
319
|
+
)
|
|
320
|
+
throw new TypeError("Local auth counters require safe integers.");
|
|
321
|
+
updated[name] = old + delta;
|
|
322
|
+
}
|
|
323
|
+
return save(input.model, updated);
|
|
324
|
+
})),
|
|
325
|
+
count: async (input) => run(() => find(input.model, input.where).length),
|
|
326
|
+
createSchema: async () =>
|
|
327
|
+
run(() => ({ code: "local_sqlite_ready", path: store.databasePath })),
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* A caught operation failure still poisons the transaction; escaped calls refuse.
|
|
333
|
+
*
|
|
334
|
+
* @template T
|
|
335
|
+
* @param {DBAdapter<BetterAuthOptions>} adapter
|
|
336
|
+
* @param {(adapter: Omit<DBAdapter<BetterAuthOptions>, "transaction">) => Promise<T>} callback
|
|
337
|
+
* @returns {Promise<T>}
|
|
338
|
+
*/
|
|
339
|
+
async function serializedCallback(adapter, callback) {
|
|
340
|
+
let queue = Promise.resolve();
|
|
341
|
+
let closed = false;
|
|
342
|
+
/** @type {unknown} */
|
|
343
|
+
let failure;
|
|
344
|
+
let failed = false;
|
|
345
|
+
let count = 0;
|
|
346
|
+
/** @param {unknown} error */
|
|
347
|
+
const poison = (error) => {
|
|
348
|
+
if (!failed) failure = error;
|
|
349
|
+
failed = true;
|
|
350
|
+
};
|
|
351
|
+
const scoped = new Proxy(adapter, {
|
|
352
|
+
get(target, property, receiver) {
|
|
353
|
+
if (property === "transaction") return undefined;
|
|
354
|
+
/** @type {unknown} */
|
|
355
|
+
const method = Reflect.get(target, property, receiver);
|
|
356
|
+
if (typeof method !== "function") return method;
|
|
357
|
+
/** @param {unknown[]} args */
|
|
358
|
+
return (...args) => {
|
|
359
|
+
if (closed)
|
|
360
|
+
return Promise.reject(
|
|
361
|
+
new Error("Local auth transaction callback has settled."),
|
|
362
|
+
);
|
|
363
|
+
if (++count > 100) {
|
|
364
|
+
const error = new Error(
|
|
365
|
+
"Local auth transaction exceeds 100 operations.",
|
|
366
|
+
);
|
|
367
|
+
poison(error);
|
|
368
|
+
return Promise.reject(error);
|
|
369
|
+
}
|
|
370
|
+
const operation = queue.then(() => {
|
|
371
|
+
if (failed) throw failure;
|
|
372
|
+
return /** @type {unknown} */ (Reflect.apply(method, target, args));
|
|
373
|
+
});
|
|
374
|
+
queue = operation.then(() => undefined, poison);
|
|
375
|
+
return operation;
|
|
376
|
+
};
|
|
377
|
+
},
|
|
378
|
+
});
|
|
379
|
+
/** @type {T | undefined} */
|
|
380
|
+
let value;
|
|
381
|
+
try {
|
|
382
|
+
value = await callback(scoped);
|
|
383
|
+
} catch (error) {
|
|
384
|
+
poison(error);
|
|
385
|
+
} finally {
|
|
386
|
+
closed = true;
|
|
387
|
+
await queue;
|
|
388
|
+
}
|
|
389
|
+
if (failed) throw failure;
|
|
390
|
+
return /** @type {T} */ (value);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export class LocalAuthConstraintError extends Error {
|
|
394
|
+
/** @readonly */
|
|
395
|
+
code = "unique_conflict";
|
|
396
|
+
constructor() {
|
|
397
|
+
super("A unique Better Auth record already exists.");
|
|
398
|
+
this.name = "LocalAuthConstraintError";
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* @param {number} value
|
|
403
|
+
* @param {number} maximum
|
|
404
|
+
* @returns {number}
|
|
405
|
+
*/
|
|
406
|
+
function bounded(value, maximum) {
|
|
407
|
+
if (!Number.isSafeInteger(value) || value < 0 || value > maximum)
|
|
408
|
+
throw new TypeError("Local auth pagination exceeds its bound.");
|
|
409
|
+
return value;
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* @param {unknown} input
|
|
413
|
+
* @returns {Row}
|
|
414
|
+
*/
|
|
415
|
+
function jsonRow(input) {
|
|
416
|
+
if (input === null || typeof input !== "object" || Array.isArray(input))
|
|
417
|
+
throw new TypeError("Auth records require objects.");
|
|
418
|
+
// Official factory handles Date conversion; undefined optional fields are omitted.
|
|
419
|
+
return /** @type {Row} */ (JSON.parse(JSON.stringify(input)));
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* @param {unknown} left
|
|
423
|
+
* @param {unknown} right
|
|
424
|
+
* @returns {boolean}
|
|
425
|
+
*/
|
|
426
|
+
function equal(left, right) {
|
|
427
|
+
return JSON.stringify(left ?? null) === JSON.stringify(right ?? null);
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* @param {unknown} left
|
|
431
|
+
* @param {unknown} right
|
|
432
|
+
* @returns {number}
|
|
433
|
+
*/
|
|
434
|
+
function compare(left, right) {
|
|
435
|
+
if (equal(left, right)) return 0;
|
|
436
|
+
if (typeof left === "number" && typeof right === "number")
|
|
437
|
+
return left < right ? -1 : 1;
|
|
438
|
+
return String(left ?? "") < String(right ?? "") ? -1 : 1;
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* @param {Row} row
|
|
442
|
+
* @param {readonly CleanedWhere[]} clauses
|
|
443
|
+
* @returns {boolean}
|
|
444
|
+
*/
|
|
445
|
+
function matches(row, clauses) {
|
|
446
|
+
let group = true;
|
|
447
|
+
let result = false;
|
|
448
|
+
for (const [index, clause] of clauses.entries()) {
|
|
449
|
+
if (index > 0 && clause.connector === "OR") {
|
|
450
|
+
result ||= group;
|
|
451
|
+
group = true;
|
|
452
|
+
}
|
|
453
|
+
let left = row[clause.field] ?? null;
|
|
454
|
+
/** @type {unknown} */
|
|
455
|
+
let right = clause.value;
|
|
456
|
+
if (clause.mode === "insensitive") {
|
|
457
|
+
if (typeof left === "string") left = left.toLowerCase();
|
|
458
|
+
right = Array.isArray(right)
|
|
459
|
+
? right.map((value) =>
|
|
460
|
+
typeof value === "string" ? value.toLowerCase() : value,
|
|
461
|
+
)
|
|
462
|
+
: typeof right === "string"
|
|
463
|
+
? right.toLowerCase()
|
|
464
|
+
: right;
|
|
465
|
+
}
|
|
466
|
+
/** @type {boolean} */
|
|
467
|
+
let matched;
|
|
468
|
+
switch (clause.operator ?? "eq") {
|
|
469
|
+
case "eq":
|
|
470
|
+
matched = equal(left, right);
|
|
471
|
+
break;
|
|
472
|
+
case "ne":
|
|
473
|
+
matched = !equal(left, right);
|
|
474
|
+
break;
|
|
475
|
+
case "lt":
|
|
476
|
+
matched = left !== null && right !== null && compare(left, right) < 0;
|
|
477
|
+
break;
|
|
478
|
+
case "lte":
|
|
479
|
+
matched = left !== null && right !== null && compare(left, right) <= 0;
|
|
480
|
+
break;
|
|
481
|
+
case "gt":
|
|
482
|
+
matched = left !== null && right !== null && compare(left, right) > 0;
|
|
483
|
+
break;
|
|
484
|
+
case "gte":
|
|
485
|
+
matched = left !== null && right !== null && compare(left, right) >= 0;
|
|
486
|
+
break;
|
|
487
|
+
case "in":
|
|
488
|
+
matched =
|
|
489
|
+
Array.isArray(right) && right.some((value) => equal(left, value));
|
|
490
|
+
break;
|
|
491
|
+
case "not_in":
|
|
492
|
+
matched =
|
|
493
|
+
Array.isArray(right) && !right.some((value) => equal(left, value));
|
|
494
|
+
break;
|
|
495
|
+
case "contains":
|
|
496
|
+
matched =
|
|
497
|
+
typeof left === "string" &&
|
|
498
|
+
typeof right === "string" &&
|
|
499
|
+
left.includes(right);
|
|
500
|
+
break;
|
|
501
|
+
case "starts_with":
|
|
502
|
+
matched =
|
|
503
|
+
typeof left === "string" &&
|
|
504
|
+
typeof right === "string" &&
|
|
505
|
+
left.startsWith(right);
|
|
506
|
+
break;
|
|
507
|
+
case "ends_with":
|
|
508
|
+
matched =
|
|
509
|
+
typeof left === "string" &&
|
|
510
|
+
typeof right === "string" &&
|
|
511
|
+
left.endsWith(right);
|
|
512
|
+
break;
|
|
513
|
+
}
|
|
514
|
+
group &&= matched;
|
|
515
|
+
}
|
|
516
|
+
return result || group;
|
|
517
|
+
}
|
package/src/local/cli.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { parseArgs } from "node:util";
|
|
4
|
+
import { createTerraScaleSqliteRuntime } from "./index.js";
|
|
5
|
+
const { values } = parseArgs({
|
|
6
|
+
options: {
|
|
7
|
+
database: { type: "string" },
|
|
8
|
+
host: { type: "string" },
|
|
9
|
+
port: { type: "string" },
|
|
10
|
+
"require-auth": { type: "boolean", default: false },
|
|
11
|
+
"allow-remote": { type: "boolean", default: false },
|
|
12
|
+
reset: { type: "boolean", default: false },
|
|
13
|
+
help: { type: "boolean", short: "h", default: false },
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
if (values.help) {
|
|
17
|
+
process.stdout.write(
|
|
18
|
+
"Usage: terrascale-local [--database PATH] [--host HOST] [--port PORT] [--require-auth] [--allow-remote] [--reset]\nExplicit local SQLite development listener for native document requests.\n",
|
|
19
|
+
);
|
|
20
|
+
} else {
|
|
21
|
+
const runtime = createTerraScaleSqliteRuntime({
|
|
22
|
+
sqlite: {
|
|
23
|
+
enabled: true,
|
|
24
|
+
databasePath: resolve(values.database ?? ".terrascale/local.sqlite"),
|
|
25
|
+
},
|
|
26
|
+
allowRemote: values["allow-remote"],
|
|
27
|
+
...(values["require-auth"]
|
|
28
|
+
? {
|
|
29
|
+
authentication: "required",
|
|
30
|
+
bearerToken: process.env.TERRASCALE_LOCAL_BEARER_TOKEN ?? "",
|
|
31
|
+
}
|
|
32
|
+
: {}),
|
|
33
|
+
});
|
|
34
|
+
try {
|
|
35
|
+
if (values.reset) await runtime.reset();
|
|
36
|
+
const address = await runtime.start({
|
|
37
|
+
...(values.host === undefined ? {} : { host: values.host }),
|
|
38
|
+
port: values.port === undefined ? 0 : Number(values.port),
|
|
39
|
+
});
|
|
40
|
+
process.stdout.write(
|
|
41
|
+
`Local SQLite development listener: ${address.origin}\nScope: ${runtime.scope.tenant}/${runtime.scope.database}/${runtime.scope.shard}\nCollection bindings must be explicitly installed by the local application.\n`,
|
|
42
|
+
);
|
|
43
|
+
for (const signal of /** @type {const} */ (["SIGINT", "SIGTERM"]))
|
|
44
|
+
process.once(signal, () => {
|
|
45
|
+
void runtime.close().then(() => process.exit(0));
|
|
46
|
+
});
|
|
47
|
+
} catch (error) {
|
|
48
|
+
await runtime.close();
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|