@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.
Files changed (182) hide show
  1. package/README.md +11 -0
  2. package/dist/Adapter.d.mts +13 -5
  3. package/dist/Adapter.mjs +9 -2
  4. package/dist/OAuthPersistence.d.mts +87 -0
  5. package/dist/OAuthPersistence.mjs +125 -0
  6. package/dist/OAuthPersistence.mjs.map +1 -0
  7. package/dist/_virtual/_rolldown/runtime.mjs +13 -0
  8. package/dist/index.d.mts +3 -1
  9. package/dist/index.mjs +3 -1
  10. package/dist/internal/authority-snapshot.mjs +53 -0
  11. package/dist/internal/authority-snapshot.mjs.map +1 -0
  12. package/dist/internal/configuration.d.mts +1 -1
  13. package/dist/internal/configuration.mjs.map +1 -1
  14. package/dist/internal/models/oauth-connected-model.d.mts +294 -0
  15. package/dist/internal/models/oauth-connected-model.mjs +22 -0
  16. package/dist/internal/models/oauth-connected-model.mjs.map +1 -0
  17. package/dist/internal/models/oauth-model.d.mts +467 -0
  18. package/dist/internal/models/oauth-model.mjs +49 -0
  19. package/dist/internal/models/oauth-model.mjs.map +1 -0
  20. package/dist/internal/models/password-model.d.mts +4 -97
  21. package/dist/internal/models/password-model.mjs +0 -3
  22. package/dist/internal/models/password-model.mjs.map +1 -1
  23. package/dist/internal/native-sql-table.d.mts +34 -0
  24. package/dist/internal/native-sql-table.mjs +111 -0
  25. package/dist/internal/native-sql-table.mjs.map +1 -0
  26. package/dist/internal/oauth/accounts.d.mts +8 -0
  27. package/dist/internal/oauth/accounts.mjs +379 -0
  28. package/dist/internal/oauth/accounts.mjs.map +1 -0
  29. package/dist/internal/oauth/connected-access.d.mts +9 -0
  30. package/dist/internal/oauth/connected-access.mjs +229 -0
  31. package/dist/internal/oauth/connected-access.mjs.map +1 -0
  32. package/dist/internal/oauth/connected-collection.d.mts +7 -0
  33. package/dist/internal/oauth/connected-collection.mjs +214 -0
  34. package/dist/internal/oauth/connected-collection.mjs.map +1 -0
  35. package/dist/internal/oauth/connected-custody.d.mts +6 -0
  36. package/dist/internal/oauth/connected-custody.mjs +165 -0
  37. package/dist/internal/oauth/connected-custody.mjs.map +1 -0
  38. package/dist/internal/oauth/connected-flow.d.mts +7 -0
  39. package/dist/internal/oauth/connected-flow.mjs +324 -0
  40. package/dist/internal/oauth/connected-flow.mjs.map +1 -0
  41. package/dist/internal/oauth/connected-input.mjs +122 -0
  42. package/dist/internal/oauth/connected-input.mjs.map +1 -0
  43. package/dist/internal/oauth/connected-keys.d.mts +2 -0
  44. package/dist/internal/oauth/connected-keys.mjs +22 -0
  45. package/dist/internal/oauth/connected-keys.mjs.map +1 -0
  46. package/dist/internal/oauth/connected-maintenance.d.mts +9 -0
  47. package/dist/internal/oauth/connected-maintenance.mjs +226 -0
  48. package/dist/internal/oauth/connected-maintenance.mjs.map +1 -0
  49. package/dist/internal/oauth/connected-management.d.mts +9 -0
  50. package/dist/internal/oauth/connected-management.mjs +243 -0
  51. package/dist/internal/oauth/connected-management.mjs.map +1 -0
  52. package/dist/internal/oauth/connected-reference.mjs +63 -0
  53. package/dist/internal/oauth/connected-reference.mjs.map +1 -0
  54. package/dist/internal/oauth/connected-settlement.d.mts +8 -0
  55. package/dist/internal/oauth/connected-settlement.mjs +141 -0
  56. package/dist/internal/oauth/connected-settlement.mjs.map +1 -0
  57. package/dist/internal/oauth/connected-sign-in.d.mts +10 -0
  58. package/dist/internal/oauth/connected-sign-in.mjs +281 -0
  59. package/dist/internal/oauth/connected-sign-in.mjs.map +1 -0
  60. package/dist/internal/oauth/connected-state.d.mts +14 -0
  61. package/dist/internal/oauth/connected-state.mjs +469 -0
  62. package/dist/internal/oauth/connected-state.mjs.map +1 -0
  63. package/dist/internal/oauth/connected-target.d.mts +21 -0
  64. package/dist/internal/oauth/connected-target.mjs +117 -0
  65. package/dist/internal/oauth/connected-target.mjs.map +1 -0
  66. package/dist/internal/oauth/flow.d.mts +5 -0
  67. package/dist/internal/oauth/flow.mjs +279 -0
  68. package/dist/internal/oauth/flow.mjs.map +1 -0
  69. package/dist/internal/oauth/input.mjs +127 -0
  70. package/dist/internal/oauth/input.mjs.map +1 -0
  71. package/dist/internal/oauth/owner.d.mts +10 -0
  72. package/dist/internal/oauth/owner.mjs +10 -0
  73. package/dist/internal/oauth/owner.mjs.map +1 -0
  74. package/dist/internal/oauth/query-compiler.d.mts +7 -0
  75. package/dist/internal/oauth/query-compiler.mjs +9 -0
  76. package/dist/internal/oauth/query-compiler.mjs.map +1 -0
  77. package/dist/internal/oauth/registration.d.mts +7 -0
  78. package/dist/internal/oauth/registration.mjs +393 -0
  79. package/dist/internal/oauth/registration.mjs.map +1 -0
  80. package/dist/internal/oauth/state.mjs +107 -0
  81. package/dist/internal/oauth/state.mjs.map +1 -0
  82. package/dist/internal/oauth/target.d.mts +31 -0
  83. package/dist/internal/oauth/target.mjs +273 -0
  84. package/dist/internal/oauth/target.mjs.map +1 -0
  85. package/dist/internal/oauth-kernel.d.mts +1423 -0
  86. package/dist/internal/oauth-kernel.mjs +187 -0
  87. package/dist/internal/oauth-kernel.mjs.map +1 -0
  88. package/dist/internal/oauth-proxy.d.mts +31 -0
  89. package/dist/internal/oauth-proxy.mjs +129 -0
  90. package/dist/internal/oauth-proxy.mjs.map +1 -0
  91. package/dist/internal/passkey/target.d.mts +1 -1
  92. package/dist/internal/passkey-kernel.d.mts +5 -5
  93. package/dist/internal/passkeys.mjs +1 -1
  94. package/dist/internal/password-credentials.d.mts +11 -0
  95. package/dist/internal/password-credentials.mjs +121 -0
  96. package/dist/internal/password-credentials.mjs.map +1 -0
  97. package/dist/internal/password-kernel.d.mts +15 -28
  98. package/dist/internal/password-kernel.mjs +35 -236
  99. package/dist/internal/password-kernel.mjs.map +1 -1
  100. package/dist/internal/persistence.d.mts +2 -0
  101. package/dist/internal/persistence.mjs +5 -5
  102. package/dist/internal/persistence.mjs.map +1 -1
  103. package/dist/internal/query-operations.d.mts +2 -0
  104. package/dist/internal/session-kernel.d.mts +5 -3
  105. package/dist/internal/session-kernel.mjs +95 -29
  106. package/dist/internal/session-kernel.mjs.map +1 -1
  107. package/dist/internal/sql-oauth-connected-model.d.mts +29 -0
  108. package/dist/internal/sql-oauth-connected-model.mjs +7 -0
  109. package/dist/internal/sql-oauth-connected-model.mjs.map +1 -0
  110. package/dist/internal/sql-oauth-model.d.mts +60 -0
  111. package/dist/internal/sql-oauth-model.mjs +10 -0
  112. package/dist/internal/sql-oauth-model.mjs.map +1 -0
  113. package/dist/internal/sql-persistence.mjs +3 -1
  114. package/dist/internal/sql-persistence.mjs.map +1 -1
  115. package/dist/internal/sql-query.d.mts +12 -0
  116. package/dist/internal/sql-query.mjs +15 -18
  117. package/dist/internal/sql-query.mjs.map +1 -1
  118. package/dist/internal/sql-storage-validation.mjs +54 -0
  119. package/dist/internal/sql-storage-validation.mjs.map +1 -0
  120. package/dist/internal/storage-mapping.mjs +2 -67
  121. package/dist/internal/storage-mapping.mjs.map +1 -1
  122. package/dist/internal/storage-plans.d.mts +7 -0
  123. package/dist/internal/storage-plans.mjs +189 -0
  124. package/dist/internal/storage-plans.mjs.map +1 -0
  125. package/dist/internal/storage-tables.d.mts +0 -99
  126. package/dist/internal/storage-tables.mjs +0 -43
  127. package/dist/internal/storage-tables.mjs.map +1 -1
  128. package/dist/internal/storage-validation.d.mts +6 -1
  129. package/dist/internal/storage-validation.mjs +42 -1
  130. package/dist/internal/storage-validation.mjs.map +1 -1
  131. package/dist/internal/transaction-execution-kernel.d.mts +1 -1
  132. package/dist/internal/transaction-kernel.d.mts +1 -1
  133. package/dist/internal/transaction-kernel.mjs +3 -2
  134. package/dist/internal/transaction-kernel.mjs.map +1 -1
  135. package/package.json +1 -1
  136. package/src/Adapter.ts +97 -8
  137. package/src/OAuthPersistence.ts +427 -0
  138. package/src/index.ts +3 -0
  139. package/src/internal/authority-snapshot.ts +91 -0
  140. package/src/internal/configuration.ts +1 -8
  141. package/src/internal/models/oauth-connected-model.ts +396 -0
  142. package/src/internal/models/oauth-model.ts +708 -0
  143. package/src/internal/models/password-model.ts +0 -120
  144. package/src/internal/native-sql-table.ts +178 -0
  145. package/src/internal/oauth/accounts.ts +838 -0
  146. package/src/internal/oauth/connected-access.ts +388 -0
  147. package/src/internal/oauth/connected-collection.ts +345 -0
  148. package/src/internal/oauth/connected-custody.ts +308 -0
  149. package/src/internal/oauth/connected-flow.ts +562 -0
  150. package/src/internal/oauth/connected-input.ts +147 -0
  151. package/src/internal/oauth/connected-keys.ts +31 -0
  152. package/src/internal/oauth/connected-maintenance.ts +379 -0
  153. package/src/internal/oauth/connected-management.ts +391 -0
  154. package/src/internal/oauth/connected-reference.ts +135 -0
  155. package/src/internal/oauth/connected-settlement.ts +278 -0
  156. package/src/internal/oauth/connected-sign-in.ts +472 -0
  157. package/src/internal/oauth/connected-state.ts +924 -0
  158. package/src/internal/oauth/connected-target.ts +307 -0
  159. package/src/internal/oauth/flow.ts +505 -0
  160. package/src/internal/oauth/input.ts +201 -0
  161. package/src/internal/oauth/owner.ts +22 -0
  162. package/src/internal/oauth/query-compiler.ts +15 -0
  163. package/src/internal/oauth/registration.ts +676 -0
  164. package/src/internal/oauth/state.ts +218 -0
  165. package/src/internal/oauth/target.ts +803 -0
  166. package/src/internal/oauth-kernel.ts +294 -0
  167. package/src/internal/oauth-proxy.ts +176 -0
  168. package/src/internal/password-credentials.ts +245 -0
  169. package/src/internal/password-kernel.ts +98 -712
  170. package/src/internal/persistence.ts +5 -10
  171. package/src/internal/query-operations.ts +2 -0
  172. package/src/internal/session-kernel.ts +231 -81
  173. package/src/internal/sql-oauth-connected-model.ts +145 -0
  174. package/src/internal/sql-oauth-model.ts +263 -0
  175. package/src/internal/sql-persistence.ts +2 -0
  176. package/src/internal/sql-query.ts +16 -13
  177. package/src/internal/sql-storage-validation.ts +64 -0
  178. package/src/internal/storage-mapping.ts +0 -66
  179. package/src/internal/storage-plans.ts +218 -0
  180. package/src/internal/storage-tables.ts +0 -35
  181. package/src/internal/storage-validation.ts +58 -0
  182. 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
+ };