@yielded/auth-persistence 0.1.0-beta.22 → 0.1.0-beta.24
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 +11 -0
- package/dist/Adapter.d.mts +13 -5
- package/dist/Adapter.mjs +9 -2
- package/dist/OAuthPersistence.d.mts +87 -0
- package/dist/OAuthPersistence.mjs +125 -0
- package/dist/OAuthPersistence.mjs.map +1 -0
- package/dist/_virtual/_rolldown/runtime.mjs +13 -0
- package/dist/index.d.mts +3 -1
- package/dist/index.mjs +3 -1
- package/dist/internal/authority-snapshot.mjs +53 -0
- package/dist/internal/authority-snapshot.mjs.map +1 -0
- package/dist/internal/configuration.d.mts +1 -1
- package/dist/internal/configuration.mjs.map +1 -1
- package/dist/internal/models/oauth-connected-model.d.mts +294 -0
- package/dist/internal/models/oauth-connected-model.mjs +22 -0
- package/dist/internal/models/oauth-connected-model.mjs.map +1 -0
- package/dist/internal/models/oauth-model.d.mts +467 -0
- package/dist/internal/models/oauth-model.mjs +49 -0
- package/dist/internal/models/oauth-model.mjs.map +1 -0
- package/dist/internal/models/password-model.d.mts +4 -97
- package/dist/internal/models/password-model.mjs +0 -3
- package/dist/internal/models/password-model.mjs.map +1 -1
- package/dist/internal/native-sql-table.d.mts +34 -0
- package/dist/internal/native-sql-table.mjs +111 -0
- package/dist/internal/native-sql-table.mjs.map +1 -0
- package/dist/internal/oauth/accounts.d.mts +8 -0
- package/dist/internal/oauth/accounts.mjs +379 -0
- package/dist/internal/oauth/accounts.mjs.map +1 -0
- package/dist/internal/oauth/connected-access.d.mts +9 -0
- package/dist/internal/oauth/connected-access.mjs +229 -0
- package/dist/internal/oauth/connected-access.mjs.map +1 -0
- package/dist/internal/oauth/connected-collection.d.mts +7 -0
- package/dist/internal/oauth/connected-collection.mjs +214 -0
- package/dist/internal/oauth/connected-collection.mjs.map +1 -0
- package/dist/internal/oauth/connected-custody.d.mts +6 -0
- package/dist/internal/oauth/connected-custody.mjs +165 -0
- package/dist/internal/oauth/connected-custody.mjs.map +1 -0
- package/dist/internal/oauth/connected-flow.d.mts +7 -0
- package/dist/internal/oauth/connected-flow.mjs +324 -0
- package/dist/internal/oauth/connected-flow.mjs.map +1 -0
- package/dist/internal/oauth/connected-input.mjs +122 -0
- package/dist/internal/oauth/connected-input.mjs.map +1 -0
- package/dist/internal/oauth/connected-keys.d.mts +2 -0
- package/dist/internal/oauth/connected-keys.mjs +22 -0
- package/dist/internal/oauth/connected-keys.mjs.map +1 -0
- package/dist/internal/oauth/connected-maintenance.d.mts +9 -0
- package/dist/internal/oauth/connected-maintenance.mjs +226 -0
- package/dist/internal/oauth/connected-maintenance.mjs.map +1 -0
- package/dist/internal/oauth/connected-management.d.mts +9 -0
- package/dist/internal/oauth/connected-management.mjs +243 -0
- package/dist/internal/oauth/connected-management.mjs.map +1 -0
- package/dist/internal/oauth/connected-reference.mjs +63 -0
- package/dist/internal/oauth/connected-reference.mjs.map +1 -0
- package/dist/internal/oauth/connected-settlement.d.mts +8 -0
- package/dist/internal/oauth/connected-settlement.mjs +141 -0
- package/dist/internal/oauth/connected-settlement.mjs.map +1 -0
- package/dist/internal/oauth/connected-sign-in.d.mts +10 -0
- package/dist/internal/oauth/connected-sign-in.mjs +281 -0
- package/dist/internal/oauth/connected-sign-in.mjs.map +1 -0
- package/dist/internal/oauth/connected-state.d.mts +14 -0
- package/dist/internal/oauth/connected-state.mjs +469 -0
- package/dist/internal/oauth/connected-state.mjs.map +1 -0
- package/dist/internal/oauth/connected-target.d.mts +21 -0
- package/dist/internal/oauth/connected-target.mjs +117 -0
- package/dist/internal/oauth/connected-target.mjs.map +1 -0
- package/dist/internal/oauth/flow.d.mts +5 -0
- package/dist/internal/oauth/flow.mjs +279 -0
- package/dist/internal/oauth/flow.mjs.map +1 -0
- package/dist/internal/oauth/input.mjs +127 -0
- package/dist/internal/oauth/input.mjs.map +1 -0
- package/dist/internal/oauth/owner.d.mts +10 -0
- package/dist/internal/oauth/owner.mjs +10 -0
- package/dist/internal/oauth/owner.mjs.map +1 -0
- package/dist/internal/oauth/query-compiler.d.mts +7 -0
- package/dist/internal/oauth/query-compiler.mjs +9 -0
- package/dist/internal/oauth/query-compiler.mjs.map +1 -0
- package/dist/internal/oauth/registration.d.mts +7 -0
- package/dist/internal/oauth/registration.mjs +393 -0
- package/dist/internal/oauth/registration.mjs.map +1 -0
- package/dist/internal/oauth/state.mjs +107 -0
- package/dist/internal/oauth/state.mjs.map +1 -0
- package/dist/internal/oauth/target.d.mts +31 -0
- package/dist/internal/oauth/target.mjs +273 -0
- package/dist/internal/oauth/target.mjs.map +1 -0
- package/dist/internal/oauth-kernel.d.mts +1423 -0
- package/dist/internal/oauth-kernel.mjs +187 -0
- package/dist/internal/oauth-kernel.mjs.map +1 -0
- package/dist/internal/oauth-proxy.d.mts +31 -0
- package/dist/internal/oauth-proxy.mjs +129 -0
- package/dist/internal/oauth-proxy.mjs.map +1 -0
- package/dist/internal/passkey/target.d.mts +1 -1
- package/dist/internal/passkey-kernel.d.mts +5 -5
- package/dist/internal/passkeys.mjs +1 -1
- package/dist/internal/password-credentials.d.mts +11 -0
- package/dist/internal/password-credentials.mjs +121 -0
- package/dist/internal/password-credentials.mjs.map +1 -0
- package/dist/internal/password-kernel.d.mts +15 -28
- package/dist/internal/password-kernel.mjs +35 -236
- package/dist/internal/password-kernel.mjs.map +1 -1
- package/dist/internal/persistence.d.mts +2 -0
- package/dist/internal/persistence.mjs +5 -5
- package/dist/internal/persistence.mjs.map +1 -1
- package/dist/internal/query-operations.d.mts +2 -0
- package/dist/internal/session-kernel.d.mts +5 -3
- package/dist/internal/session-kernel.mjs +95 -29
- package/dist/internal/session-kernel.mjs.map +1 -1
- package/dist/internal/sql-oauth-connected-model.d.mts +29 -0
- package/dist/internal/sql-oauth-connected-model.mjs +7 -0
- package/dist/internal/sql-oauth-connected-model.mjs.map +1 -0
- package/dist/internal/sql-oauth-model.d.mts +60 -0
- package/dist/internal/sql-oauth-model.mjs +10 -0
- package/dist/internal/sql-oauth-model.mjs.map +1 -0
- package/dist/internal/sql-persistence.mjs +3 -1
- package/dist/internal/sql-persistence.mjs.map +1 -1
- package/dist/internal/sql-query.d.mts +12 -0
- package/dist/internal/sql-query.mjs +15 -18
- package/dist/internal/sql-query.mjs.map +1 -1
- package/dist/internal/sql-storage-validation.mjs +54 -0
- package/dist/internal/sql-storage-validation.mjs.map +1 -0
- package/dist/internal/storage-mapping.mjs +2 -67
- package/dist/internal/storage-mapping.mjs.map +1 -1
- package/dist/internal/storage-plans.d.mts +7 -0
- package/dist/internal/storage-plans.mjs +189 -0
- package/dist/internal/storage-plans.mjs.map +1 -0
- package/dist/internal/storage-tables.d.mts +0 -99
- package/dist/internal/storage-tables.mjs +0 -43
- package/dist/internal/storage-tables.mjs.map +1 -1
- package/dist/internal/storage-validation.d.mts +6 -1
- package/dist/internal/storage-validation.mjs +42 -1
- package/dist/internal/storage-validation.mjs.map +1 -1
- package/dist/internal/transaction-execution-kernel.d.mts +1 -1
- package/dist/internal/transaction-kernel.d.mts +1 -1
- package/dist/internal/transaction-kernel.mjs +3 -2
- package/dist/internal/transaction-kernel.mjs.map +1 -1
- package/package.json +1 -1
- package/src/Adapter.ts +97 -8
- package/src/OAuthPersistence.ts +427 -0
- package/src/index.ts +3 -0
- package/src/internal/authority-snapshot.ts +91 -0
- package/src/internal/configuration.ts +1 -8
- package/src/internal/models/oauth-connected-model.ts +396 -0
- package/src/internal/models/oauth-model.ts +708 -0
- package/src/internal/models/password-model.ts +0 -120
- package/src/internal/native-sql-table.ts +178 -0
- package/src/internal/oauth/accounts.ts +838 -0
- package/src/internal/oauth/connected-access.ts +388 -0
- package/src/internal/oauth/connected-collection.ts +345 -0
- package/src/internal/oauth/connected-custody.ts +308 -0
- package/src/internal/oauth/connected-flow.ts +562 -0
- package/src/internal/oauth/connected-input.ts +147 -0
- package/src/internal/oauth/connected-keys.ts +31 -0
- package/src/internal/oauth/connected-maintenance.ts +379 -0
- package/src/internal/oauth/connected-management.ts +391 -0
- package/src/internal/oauth/connected-reference.ts +135 -0
- package/src/internal/oauth/connected-settlement.ts +278 -0
- package/src/internal/oauth/connected-sign-in.ts +472 -0
- package/src/internal/oauth/connected-state.ts +924 -0
- package/src/internal/oauth/connected-target.ts +307 -0
- package/src/internal/oauth/flow.ts +505 -0
- package/src/internal/oauth/input.ts +201 -0
- package/src/internal/oauth/owner.ts +22 -0
- package/src/internal/oauth/query-compiler.ts +15 -0
- package/src/internal/oauth/registration.ts +676 -0
- package/src/internal/oauth/state.ts +218 -0
- package/src/internal/oauth/target.ts +803 -0
- package/src/internal/oauth-kernel.ts +294 -0
- package/src/internal/oauth-proxy.ts +176 -0
- package/src/internal/password-credentials.ts +245 -0
- package/src/internal/password-kernel.ts +98 -712
- package/src/internal/persistence.ts +5 -10
- package/src/internal/query-operations.ts +2 -0
- package/src/internal/session-kernel.ts +231 -81
- package/src/internal/sql-oauth-connected-model.ts +145 -0
- package/src/internal/sql-oauth-model.ts +263 -0
- package/src/internal/sql-persistence.ts +2 -0
- package/src/internal/sql-query.ts +16 -13
- package/src/internal/sql-storage-validation.ts +64 -0
- package/src/internal/storage-mapping.ts +0 -66
- package/src/internal/storage-plans.ts +218 -0
- package/src/internal/storage-tables.ts +0 -35
- package/src/internal/storage-validation.ts +58 -0
- package/src/internal/transaction-kernel.ts +6 -2
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { type RecoveryReference, type LoginIdentifier } from "@yielded/auth/Identity";
|
|
2
2
|
import type {
|
|
3
3
|
PasswordAction,
|
|
4
|
-
PasswordAttemptId,
|
|
5
4
|
PasswordCredentialSnapshot,
|
|
6
5
|
PasswordReplacement,
|
|
7
6
|
PasswordRegistrationDecision,
|
|
@@ -15,18 +14,6 @@ import type { PersistenceMappingError, SubjectIdCodec } from "./common";
|
|
|
15
14
|
|
|
16
15
|
type ColumnKey<T extends Table> = T["column"];
|
|
17
16
|
|
|
18
|
-
export type PasswordAttemptAction = "sign-in" | "change";
|
|
19
|
-
|
|
20
|
-
export type PasswordAttemptState = "pending" | "verified" | "rejected";
|
|
21
|
-
|
|
22
|
-
export type PasswordRateScopeKind = "action" | "identifier" | "subject";
|
|
23
|
-
|
|
24
|
-
export interface PasswordScopeKeys {
|
|
25
|
-
readonly action: string;
|
|
26
|
-
readonly identifier: string;
|
|
27
|
-
readonly subject?: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
17
|
export interface PasswordSubjectTable<Subject extends Table, _NativeSubjectId> {
|
|
31
18
|
readonly table: Subject["table"];
|
|
32
19
|
readonly id: ColumnKey<Subject>;
|
|
@@ -129,82 +116,6 @@ export interface PasswordCredentialTable<
|
|
|
129
116
|
}) => Effect.Effect<PasswordCredentialSnapshot, PersistenceMappingError>;
|
|
130
117
|
}
|
|
131
118
|
|
|
132
|
-
export interface PasswordAttemptRecord {
|
|
133
|
-
readonly moduleId: string;
|
|
134
|
-
readonly action: PasswordAttemptAction;
|
|
135
|
-
readonly attemptId: PasswordAttemptId;
|
|
136
|
-
readonly identifier: LoginIdentifier;
|
|
137
|
-
readonly subjectId?: string;
|
|
138
|
-
readonly credentialId?: string;
|
|
139
|
-
readonly securityRevision?: SecurityRevision;
|
|
140
|
-
readonly credentialRevision?: SecurityRevision;
|
|
141
|
-
readonly verifierVersion?: SecurityRevision;
|
|
142
|
-
readonly identifierBindingRevision?: SecurityRevision;
|
|
143
|
-
readonly admittedAtMillis: number;
|
|
144
|
-
readonly deadlineMillis: number;
|
|
145
|
-
readonly retentionUntilMillis: number;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
export interface PasswordAttemptTable<Attempt extends Table, NativeSubjectId> {
|
|
149
|
-
readonly table: Attempt["table"];
|
|
150
|
-
readonly moduleId: ColumnKey<Attempt>;
|
|
151
|
-
readonly action: ColumnKey<Attempt>;
|
|
152
|
-
readonly attemptId: ColumnKey<Attempt>;
|
|
153
|
-
readonly identifierNamespace: ColumnKey<Attempt>;
|
|
154
|
-
readonly identifierValue: ColumnKey<Attempt>;
|
|
155
|
-
readonly subjectId: ColumnKey<Attempt>;
|
|
156
|
-
readonly credentialId: ColumnKey<Attempt>;
|
|
157
|
-
readonly securityRevision: ColumnKey<Attempt>;
|
|
158
|
-
readonly credentialRevision: ColumnKey<Attempt>;
|
|
159
|
-
readonly verifierVersion: ColumnKey<Attempt>;
|
|
160
|
-
readonly identifierBindingRevision: ColumnKey<Attempt>;
|
|
161
|
-
readonly admittedAt: ColumnKey<Attempt>;
|
|
162
|
-
readonly deadline: ColumnKey<Attempt>;
|
|
163
|
-
readonly retentionUntil: ColumnKey<Attempt>;
|
|
164
|
-
readonly state: ColumnKey<Attempt>;
|
|
165
|
-
readonly encodeInsert: (
|
|
166
|
-
record: PasswordAttemptRecord,
|
|
167
|
-
input: {
|
|
168
|
-
readonly nativeSubjectId?: NativeSubjectId;
|
|
169
|
-
readonly state: "pending";
|
|
170
|
-
},
|
|
171
|
-
) => Attempt["insert"];
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
export interface PasswordRateScopeTable<RateScope extends Table> {
|
|
175
|
-
readonly table: RateScope["table"];
|
|
176
|
-
readonly moduleId: ColumnKey<RateScope>;
|
|
177
|
-
readonly action: ColumnKey<RateScope>;
|
|
178
|
-
readonly scopeKind: ColumnKey<RateScope>;
|
|
179
|
-
readonly scopeKey: ColumnKey<RateScope>;
|
|
180
|
-
readonly encodeInsert: (input: {
|
|
181
|
-
readonly moduleId: string;
|
|
182
|
-
readonly action: PasswordAttemptAction;
|
|
183
|
-
readonly scopeKind: PasswordRateScopeKind;
|
|
184
|
-
readonly scopeKey: string;
|
|
185
|
-
}) => RateScope["insert"];
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
export interface PasswordChargeTable<Charge extends Table> {
|
|
189
|
-
readonly table: Charge["table"];
|
|
190
|
-
readonly moduleId: ColumnKey<Charge>;
|
|
191
|
-
readonly action: ColumnKey<Charge>;
|
|
192
|
-
readonly scopeKind: ColumnKey<Charge>;
|
|
193
|
-
readonly scopeKey: ColumnKey<Charge>;
|
|
194
|
-
readonly attemptId: ColumnKey<Charge>;
|
|
195
|
-
readonly occurredAt: ColumnKey<Charge>;
|
|
196
|
-
readonly retentionUntil: ColumnKey<Charge>;
|
|
197
|
-
readonly encodeInsert: (input: {
|
|
198
|
-
readonly moduleId: string;
|
|
199
|
-
readonly action: PasswordAttemptAction;
|
|
200
|
-
readonly scopeKind: PasswordRateScopeKind;
|
|
201
|
-
readonly scopeKey: string;
|
|
202
|
-
readonly attemptId: PasswordAttemptId;
|
|
203
|
-
readonly occurredAtMillis: number;
|
|
204
|
-
readonly retentionUntilMillis: number;
|
|
205
|
-
}) => Charge["insert"];
|
|
206
|
-
}
|
|
207
|
-
|
|
208
119
|
export interface PasswordCommandTable<Command extends Table> {
|
|
209
120
|
readonly table: Command["table"];
|
|
210
121
|
readonly moduleId: ColumnKey<Command>;
|
|
@@ -228,9 +139,6 @@ export interface RequiredPasswordConstraints {
|
|
|
228
139
|
readonly authorityCredential: "unique(authorityCredential.subjectId,authorityCredential.credentialId)";
|
|
229
140
|
readonly credentialSubject: "unique(credential.moduleId,credential.subjectId)";
|
|
230
141
|
readonly credentialId: "unique(credential.moduleId,credential.credentialId)";
|
|
231
|
-
readonly attempt: "unique(attempt.moduleId,attempt.attemptId)";
|
|
232
|
-
readonly rateScope: "unique(rateScope.moduleId,rateScope.action,rateScope.scopeKind,rateScope.scopeKey)";
|
|
233
|
-
readonly charge: "unique(charge.moduleId,charge.action,charge.scopeKind,charge.scopeKey,charge.attemptId)";
|
|
234
142
|
readonly command: "unique(command.moduleId,command.commandId)";
|
|
235
143
|
}
|
|
236
144
|
|
|
@@ -239,17 +147,12 @@ export const requiredPasswordConstraints: RequiredPasswordConstraints = {
|
|
|
239
147
|
authorityCredential: "unique(authorityCredential.subjectId,authorityCredential.credentialId)",
|
|
240
148
|
credentialSubject: "unique(credential.moduleId,credential.subjectId)",
|
|
241
149
|
credentialId: "unique(credential.moduleId,credential.credentialId)",
|
|
242
|
-
attempt: "unique(attempt.moduleId,attempt.attemptId)",
|
|
243
|
-
rateScope: "unique(rateScope.moduleId,rateScope.action,rateScope.scopeKind,rateScope.scopeKey)",
|
|
244
|
-
charge: "unique(charge.moduleId,charge.action,charge.scopeKind,charge.scopeKey,charge.attemptId)",
|
|
245
150
|
command: "unique(command.moduleId,command.commandId)",
|
|
246
151
|
};
|
|
247
152
|
|
|
248
153
|
export interface PasswordConstraintClassifier {
|
|
249
154
|
/** Match only the mapped command uniqueness constraint. */
|
|
250
155
|
readonly isCommandConflict: (cause: unknown) => boolean;
|
|
251
|
-
/** Match only the mapped rate-scope uniqueness constraint. */
|
|
252
|
-
readonly isRateScopeConflict: (cause: unknown) => boolean;
|
|
253
156
|
}
|
|
254
157
|
|
|
255
158
|
export interface PasswordD1Clock<Expression extends SqlExpression = SqlExpression> {
|
|
@@ -264,9 +167,6 @@ export interface PasswordPersistenceMapping<
|
|
|
264
167
|
Identifier extends Table,
|
|
265
168
|
Credential extends Table,
|
|
266
169
|
AuthorityCredential extends Table,
|
|
267
|
-
Attempt extends Table,
|
|
268
|
-
RateScope extends Table,
|
|
269
|
-
Charge extends Table,
|
|
270
170
|
Command extends Table,
|
|
271
171
|
NativeSubjectId,
|
|
272
172
|
Expression extends SqlExpression = SqlExpression,
|
|
@@ -279,22 +179,11 @@ export interface PasswordPersistenceMapping<
|
|
|
279
179
|
AuthorityCredential,
|
|
280
180
|
NativeSubjectId
|
|
281
181
|
>;
|
|
282
|
-
readonly attempt: PasswordAttemptTable<Attempt, NativeSubjectId>;
|
|
283
|
-
readonly rateScope: PasswordRateScopeTable<RateScope>;
|
|
284
|
-
readonly charge: PasswordChargeTable<Charge>;
|
|
285
182
|
readonly command: PasswordCommandTable<Command>;
|
|
286
183
|
readonly subjectId: SubjectIdCodec<NativeSubjectId>;
|
|
287
184
|
readonly constraints: RequiredPasswordConstraints;
|
|
288
|
-
readonly scopeKeys: (input: {
|
|
289
|
-
readonly moduleId: string;
|
|
290
|
-
readonly action: PasswordAttemptAction;
|
|
291
|
-
readonly identifier: LoginIdentifier;
|
|
292
|
-
readonly subjectId?: string;
|
|
293
|
-
}) => PasswordScopeKeys;
|
|
294
185
|
readonly encodeInstant: (epochMillis: number) => unknown;
|
|
295
186
|
readonly decodeInstant: (native: unknown) => Effect.Effect<number, PersistenceMappingError>;
|
|
296
|
-
readonly allocateAttemptId?: Effect.Effect<PasswordAttemptId, PersistenceMappingError>;
|
|
297
|
-
readonly allocateAttemptIdSync?: () => PasswordAttemptId;
|
|
298
187
|
readonly allocateCredentialId?: Effect.Effect<string, PersistenceMappingError>;
|
|
299
188
|
readonly allocateCredentialIdSync?: () => string;
|
|
300
189
|
readonly allocateRevision?: Effect.Effect<SecurityRevision, PersistenceMappingError>;
|
|
@@ -310,9 +199,6 @@ export type D1PasswordPersistenceMapping<
|
|
|
310
199
|
Identifier extends Table,
|
|
311
200
|
Credential extends Table,
|
|
312
201
|
AuthorityCredential extends Table,
|
|
313
|
-
Attempt extends Table,
|
|
314
|
-
RateScope extends Table,
|
|
315
|
-
Charge extends Table,
|
|
316
202
|
Command extends Table,
|
|
317
203
|
NativeSubjectId,
|
|
318
204
|
Expression extends SqlExpression = SqlExpression,
|
|
@@ -321,9 +207,6 @@ export type D1PasswordPersistenceMapping<
|
|
|
321
207
|
Identifier,
|
|
322
208
|
Credential,
|
|
323
209
|
AuthorityCredential,
|
|
324
|
-
Attempt,
|
|
325
|
-
RateScope,
|
|
326
|
-
Charge,
|
|
327
210
|
Command,
|
|
328
211
|
NativeSubjectId,
|
|
329
212
|
Expression
|
|
@@ -491,9 +374,6 @@ export type AnyPasswordPersistenceMapping<Expression extends SqlExpression = Sql
|
|
|
491
374
|
AnyTableModel,
|
|
492
375
|
AnyTableModel,
|
|
493
376
|
AnyTableModel,
|
|
494
|
-
AnyTableModel,
|
|
495
|
-
AnyTableModel,
|
|
496
|
-
AnyTableModel,
|
|
497
377
|
unknown,
|
|
498
378
|
Expression
|
|
499
379
|
>;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import type { SqlClient } from "effect/sql/SqlClient";
|
|
3
|
+
import * as Statement from "effect/sql/Statement";
|
|
4
|
+
|
|
5
|
+
import { PersistenceMappingError } from "./mapping-error";
|
|
6
|
+
import { Table, identifier } from "./sql-table";
|
|
7
|
+
import type { PhysicalTextColumn } from "./storage-validation";
|
|
8
|
+
|
|
9
|
+
/** Capture synchronous mapping callbacks without changing SQL execution errors. */
|
|
10
|
+
export const sqlMapping = <A>(thunk: () => A): Effect.Effect<A, PersistenceMappingError> =>
|
|
11
|
+
Effect.try({
|
|
12
|
+
try: thunk,
|
|
13
|
+
catch: (cause) => PersistenceMappingError.make({ operation: "mapping", cause }),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
/** Physical SQL representation only. Semantic row codecs and policy remain in
|
|
17
|
+
* the owning mapping. Execute assembled statements without name/result transforms. */
|
|
18
|
+
export interface SqlTable {
|
|
19
|
+
readonly name: Statement.Fragment;
|
|
20
|
+
/** Alias reads only; writes continue to target the original physical table. */
|
|
21
|
+
readonly as: (alias: string) => SqlTable;
|
|
22
|
+
readonly column: (key: string) => Statement.Fragment;
|
|
23
|
+
/** Optional codec-free text candidate. Physical type/collation compatibility
|
|
24
|
+
* must still be checked against the database before comparing columns. */
|
|
25
|
+
readonly unencodedTextColumn?: (key: string) => PhysicalTextColumn | undefined;
|
|
26
|
+
/** Project every column under an ordinal alias, using a distinct prefix per table. */
|
|
27
|
+
readonly fields: (prefix: string) => Statement.Fragment;
|
|
28
|
+
readonly decode: (
|
|
29
|
+
row: Readonly<Record<string, unknown>>,
|
|
30
|
+
prefix: string,
|
|
31
|
+
) => Record<string, unknown>;
|
|
32
|
+
readonly value: (key: string, value: unknown) => Statement.Fragment;
|
|
33
|
+
/** Complete INSERT prefix; append conflict/returning clauses in the owning operation. */
|
|
34
|
+
readonly insert: (values: Readonly<Record<string, unknown>>) => Statement.Fragment;
|
|
35
|
+
/** Complete UPDATE ... SET prefix; the owning operation supplies its predicate. */
|
|
36
|
+
readonly update: (values: Readonly<Record<string, unknown>>) => Statement.Fragment;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type NativeSqlTables = (table: object) => SqlTable;
|
|
40
|
+
|
|
41
|
+
/** Prefixes are short ASCII identifiers so ordinal aliases cannot be truncated
|
|
42
|
+
* by PostgreSQL. Callers distinguish tables by prefix, never by physical names. */
|
|
43
|
+
export const nativeSqlAlias = (prefix: string, index: number): string => {
|
|
44
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]{0,39}$/.test(prefix))
|
|
45
|
+
throw PersistenceMappingError.make({ operation: "mapping", cause: "Invalid SQL alias prefix" });
|
|
46
|
+
|
|
47
|
+
return `${prefix}${index}`;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const directTable = (client: SqlClient, physical: object): SqlTable => {
|
|
51
|
+
if (!(physical instanceof Table))
|
|
52
|
+
throw PersistenceMappingError.make({ operation: "mapping", cause: "Expected a SQL table" });
|
|
53
|
+
|
|
54
|
+
const quote = (name: string) => {
|
|
55
|
+
if (name.includes("\0"))
|
|
56
|
+
throw PersistenceMappingError.make({ operation: "mapping", cause: "NUL in SQL identifier" });
|
|
57
|
+
|
|
58
|
+
return client.literal(identifier(name));
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const name =
|
|
62
|
+
physical.schema === undefined
|
|
63
|
+
? quote(physical.name)
|
|
64
|
+
: client`${quote(physical.schema)}.${quote(physical.name)}`;
|
|
65
|
+
|
|
66
|
+
const columns = Object.entries(physical.columns);
|
|
67
|
+
|
|
68
|
+
const getColumn = (key: string) => {
|
|
69
|
+
const column = Object.hasOwn(physical.columns, key) ? physical.columns[key] : undefined;
|
|
70
|
+
|
|
71
|
+
if (column === undefined)
|
|
72
|
+
throw PersistenceMappingError.make({ operation: "column", cause: "Missing SQL column" });
|
|
73
|
+
|
|
74
|
+
return column;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const value = (key: string, input: unknown): Statement.Fragment => {
|
|
78
|
+
const mapped = getColumn(key);
|
|
79
|
+
|
|
80
|
+
if (Statement.isFragment(input)) return input;
|
|
81
|
+
const encoded = input === null ? null : mapped.mapToDriverValue(input);
|
|
82
|
+
|
|
83
|
+
return Statement.fragment([
|
|
84
|
+
Statement.parameter(
|
|
85
|
+
typeof encoded === "boolean"
|
|
86
|
+
? client.onDialectOrElse({ sqlite: () => Number(encoded), orElse: () => encoded })
|
|
87
|
+
: encoded,
|
|
88
|
+
),
|
|
89
|
+
]);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const entries = (values: Readonly<Record<string, unknown>>) =>
|
|
93
|
+
Object.entries(values).filter(
|
|
94
|
+
([key, input]) => Object.hasOwn(physical.columns, key) && input !== undefined,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const bind = (alias?: string): SqlTable => {
|
|
98
|
+
const reference = alias === undefined ? name : quote(alias);
|
|
99
|
+
const column = (key: string) => client`${reference}.${quote(getColumn(key).options.name)}`;
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
name: alias === undefined ? name : client`${name} AS ${reference}`,
|
|
103
|
+
as: bind,
|
|
104
|
+
column,
|
|
105
|
+
unencodedTextColumn: (key) =>
|
|
106
|
+
getColumn(key).options.type === "text"
|
|
107
|
+
? {
|
|
108
|
+
table: {
|
|
109
|
+
name: physical.name,
|
|
110
|
+
...(physical.schema === undefined ? {} : { schema: physical.schema }),
|
|
111
|
+
},
|
|
112
|
+
name: getColumn(key).options.name,
|
|
113
|
+
}
|
|
114
|
+
: undefined,
|
|
115
|
+
value,
|
|
116
|
+
fields: (prefix) =>
|
|
117
|
+
client.join(
|
|
118
|
+
", ",
|
|
119
|
+
false,
|
|
120
|
+
)(
|
|
121
|
+
columns.map(
|
|
122
|
+
([key], index) => client`${column(key)} AS ${quote(nativeSqlAlias(prefix, index))}`,
|
|
123
|
+
),
|
|
124
|
+
),
|
|
125
|
+
decode: (row, prefix) =>
|
|
126
|
+
Object.fromEntries(
|
|
127
|
+
columns.map(([key, field], index) => {
|
|
128
|
+
const alias = nativeSqlAlias(prefix, index);
|
|
129
|
+
|
|
130
|
+
if (!Object.hasOwn(row, alias))
|
|
131
|
+
throw PersistenceMappingError.make({
|
|
132
|
+
operation: "decode",
|
|
133
|
+
cause: "Missing SQL projection field",
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
return [key, field.decode(row[alias])];
|
|
137
|
+
}),
|
|
138
|
+
),
|
|
139
|
+
insert: (values) => {
|
|
140
|
+
const fields = entries(values);
|
|
141
|
+
|
|
142
|
+
if (fields.length === 0) return client`INSERT INTO ${name} DEFAULT VALUES`;
|
|
143
|
+
|
|
144
|
+
return client`INSERT INTO ${name} (${client.join(", ", false)(fields.map(([key]) => quote(getColumn(key).options.name)))}) VALUES (${client.join(", ", false)(fields.map(([key, input]) => value(key, input)))})`;
|
|
145
|
+
},
|
|
146
|
+
update: (values) => {
|
|
147
|
+
const fields = entries(values);
|
|
148
|
+
|
|
149
|
+
if (fields.length === 0)
|
|
150
|
+
throw PersistenceMappingError.make({ operation: "mapping", cause: "Empty SQL update" });
|
|
151
|
+
|
|
152
|
+
return client`UPDATE ${name} SET ${client.join(", ", false)(fields.map(([key, input]) => client`${quote(getColumn(key).options.name)} = ${value(key, input)}`))}`;
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
return bind();
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/** Cache physical metadata only; insert/update materialize values and hooks at
|
|
161
|
+
* each call. Construction acquires no client and owns no transaction. */
|
|
162
|
+
export const makeNativeSqlTables = (
|
|
163
|
+
client: SqlClient,
|
|
164
|
+
convert: (client: SqlClient, table: object) => SqlTable = directTable,
|
|
165
|
+
): NativeSqlTables => {
|
|
166
|
+
const tables = new WeakMap<object, SqlTable>();
|
|
167
|
+
|
|
168
|
+
return (table) => {
|
|
169
|
+
const existing = tables.get(table);
|
|
170
|
+
|
|
171
|
+
if (existing !== undefined) return existing;
|
|
172
|
+
const mapped = convert(client, table);
|
|
173
|
+
|
|
174
|
+
tables.set(table, mapped);
|
|
175
|
+
|
|
176
|
+
return mapped;
|
|
177
|
+
};
|
|
178
|
+
};
|