@yielded/auth-persistence 0.1.0-beta.13 → 0.1.0-beta.15
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/dist/Adapter.d.mts +5 -3
- package/dist/Adapter.mjs +3 -1
- package/dist/index.d.mts +2 -1
- package/dist/index.mjs +2 -1
- package/dist/internal/browser-login.d.mts +10 -0
- package/dist/internal/browser-login.mjs +67 -0
- package/dist/internal/browser-login.mjs.map +1 -0
- package/dist/internal/email-kernel.d.mts +4 -3
- package/dist/internal/email-kernel.mjs +8 -0
- package/dist/internal/email-kernel.mjs.map +1 -1
- package/dist/internal/models/email-model.d.mts +5 -1
- package/dist/internal/models/email-model.mjs.map +1 -1
- package/dist/internal/models/passkey-model.d.mts +1 -1
- package/dist/internal/models/passkey-write-model.d.mts +1 -1
- package/dist/internal/models/password-model.d.mts +1 -1
- package/dist/internal/models/proof-model.d.mts +1 -1
- package/dist/internal/models/session-model.d.mts +1 -1
- package/dist/internal/models/step-up-model.d.mts +1 -1
- package/dist/internal/passkey/admission.d.mts +1 -1
- package/dist/internal/passkey/credentials.d.mts +1 -1
- package/dist/internal/passkey/flow.d.mts +1 -1
- package/dist/internal/passkey/registration-ceremony.d.mts +1 -1
- package/dist/internal/passkey/state.d.mts +1 -1
- package/dist/internal/passkey/target.d.mts +2 -1
- package/dist/internal/passkey/target.mjs +9 -8
- package/dist/internal/passkey/target.mjs.map +1 -1
- package/dist/internal/passkey/write-state.d.mts +1 -1
- package/dist/internal/passkey/write-target.mjs +4 -4
- package/dist/internal/passkey/write-target.mjs.map +1 -1
- package/dist/internal/passkey-kernel.d.mts +9 -9
- package/dist/internal/passkey-kernel.mjs +1 -1
- package/dist/internal/passkey-kernel.mjs.map +1 -1
- package/dist/internal/password-kernel.d.mts +4 -3
- package/dist/internal/password-kernel.mjs +4 -0
- package/dist/internal/password-kernel.mjs.map +1 -1
- package/dist/internal/persistence.d.mts +1 -1
- package/dist/internal/persistence.mjs +6 -6
- package/dist/internal/persistence.mjs.map +1 -1
- package/dist/internal/phone-kernel.d.mts +1 -1
- package/dist/internal/proof-kernel.d.mts +3 -2
- package/dist/internal/proof-kernel.mjs +6 -2
- package/dist/internal/proof-kernel.mjs.map +1 -1
- package/dist/internal/query-operations.d.mts +9 -4
- package/dist/internal/session-kernel.d.mts +7 -5
- package/dist/internal/session-kernel.mjs +9 -1
- package/dist/internal/session-kernel.mjs.map +1 -1
- package/dist/internal/storage-mapping.d.mts +59 -0
- package/dist/internal/storage-mapping.mjs +5 -0
- package/dist/internal/storage-mapping.mjs.map +1 -1
- package/dist/internal/storage-validation.d.mts +18 -0
- package/dist/internal/storage-validation.mjs +26 -1
- package/dist/internal/storage-validation.mjs.map +1 -1
- package/dist/internal/transaction-execution-kernel.d.mts +1 -1
- package/dist/internal/transaction-execution-kernel.mjs.map +1 -1
- package/dist/internal/transaction-kernel.d.mts +1 -1
- package/dist/internal/transaction-kernel.mjs +1 -1
- package/dist/internal/transaction-kernel.mjs.map +1 -1
- package/package.json +1 -1
- package/src/Adapter.ts +8 -4
- package/src/index.ts +1 -0
- package/src/internal/browser-login.ts +84 -0
- package/src/internal/email-kernel.ts +27 -2
- package/src/internal/models/email-model.ts +4 -0
- package/src/internal/passkey/target.ts +20 -9
- package/src/internal/passkey/write-target.ts +8 -8
- package/src/internal/passkey-kernel.ts +9 -1
- package/src/internal/password-kernel.ts +14 -1
- package/src/internal/persistence.ts +27 -6
- package/src/internal/proof-kernel.ts +50 -12
- package/src/internal/query-operations.ts +8 -0
- package/src/internal/session-kernel.ts +63 -8
- package/src/internal/storage-mapping.ts +5 -0
- package/src/internal/storage-validation.ts +83 -19
- package/src/internal/transaction-execution-kernel.ts +3 -2
- package/src/internal/transaction-kernel.ts +1 -1
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
import type { TableModel as Table } from "./query-operations";
|
|
44
44
|
import type { QueryFailure } from "./query-operations";
|
|
45
45
|
import type { QueryOperations, SqlFragment, SqlColumn } from "./query-operations";
|
|
46
|
+
import type { NativeDatabase } from "./transaction-kernel";
|
|
46
47
|
|
|
47
48
|
type AdapterFailure = QueryFailure | PersistenceMappingError | SqlError.SqlError;
|
|
48
49
|
|
|
@@ -661,8 +662,17 @@ export const makeProofKernel = <
|
|
|
661
662
|
const makeSqlProofPersistence = Effect.fn("makeSqlProofPersistence")(function* (
|
|
662
663
|
mapping: Mapping,
|
|
663
664
|
configuration: ProofSqlConfiguration,
|
|
664
|
-
): Effect.fn.Return<
|
|
665
|
+
): Effect.fn.Return<
|
|
666
|
+
ProofPersistence["Service"],
|
|
667
|
+
ProofUnavailable,
|
|
668
|
+
LifecycleHooks | CurrentProofSql | NativeDatabase
|
|
669
|
+
> {
|
|
665
670
|
const database = yield* CurrentProofSql;
|
|
671
|
+
|
|
672
|
+
if (!configuration.coordinated)
|
|
673
|
+
yield* (operations.validateStorage?.(mapping) ?? Effect.void).pipe(
|
|
674
|
+
Effect.mapError(unavailable),
|
|
675
|
+
);
|
|
666
676
|
const hooks = yield* LifecycleHooks;
|
|
667
677
|
|
|
668
678
|
return ProofPersistence.of({
|
|
@@ -835,6 +845,34 @@ export const makeProofKernel = <
|
|
|
835
845
|
| null
|
|
836
846
|
| undefined;
|
|
837
847
|
|
|
848
|
+
const activeGeneration =
|
|
849
|
+
activeProofId === null || activeProofId === undefined
|
|
850
|
+
? undefined
|
|
851
|
+
: (yield* readGeneration(
|
|
852
|
+
mapping,
|
|
853
|
+
input.record.moduleId,
|
|
854
|
+
activeProofId,
|
|
855
|
+
configuration.locking,
|
|
856
|
+
))[0];
|
|
857
|
+
|
|
858
|
+
if (
|
|
859
|
+
activeProofId !== null &&
|
|
860
|
+
activeProofId !== undefined &&
|
|
861
|
+
(activeGeneration === undefined ||
|
|
862
|
+
activeGeneration[mapping.generation.purpose] !== input.record.purpose)
|
|
863
|
+
)
|
|
864
|
+
return yield* unavailable();
|
|
865
|
+
|
|
866
|
+
const replacementAuthorized =
|
|
867
|
+
activeGeneration === undefined ||
|
|
868
|
+
activeGeneration[mapping.generation.state] !== "active" ||
|
|
869
|
+
(yield* mapping.decodeInstant(activeGeneration[mapping.generation.expiresAt])) <=
|
|
870
|
+
now ||
|
|
871
|
+
sameBinding(
|
|
872
|
+
yield* mapping.generation.decodeBinding(activeGeneration),
|
|
873
|
+
input.record.binding,
|
|
874
|
+
);
|
|
875
|
+
|
|
838
876
|
const supersedesCurrent =
|
|
839
877
|
input.supersedes === undefined || input.supersedes === activeProofId;
|
|
840
878
|
|
|
@@ -844,6 +882,7 @@ export const makeProofKernel = <
|
|
|
844
882
|
admitted.length === scopes.length &&
|
|
845
883
|
input.record.issuedAtMillis <= now &&
|
|
846
884
|
input.record.expiresAtMillis > now &&
|
|
885
|
+
replacementAuthorized &&
|
|
847
886
|
supersedesCurrent &&
|
|
848
887
|
(lastIssueAt === undefined ||
|
|
849
888
|
now - lastIssueAt >= input.policy.abuse.resendCooldownMillis);
|
|
@@ -864,18 +903,17 @@ export const makeProofKernel = <
|
|
|
864
903
|
journal,
|
|
865
904
|
);
|
|
866
905
|
|
|
867
|
-
if (admitted.length > 0)
|
|
868
|
-
yield* insertScopeEvents(
|
|
869
|
-
mapping,
|
|
870
|
-
input.record.moduleId,
|
|
871
|
-
input.record.purpose,
|
|
872
|
-
"issue",
|
|
873
|
-
input.record.requestId,
|
|
874
|
-
admitted,
|
|
875
|
-
now,
|
|
876
|
-
retentionUntil,
|
|
877
|
-
);
|
|
878
906
|
if (!allowed) return prepared;
|
|
907
|
+
yield* insertScopeEvents(
|
|
908
|
+
mapping,
|
|
909
|
+
input.record.moduleId,
|
|
910
|
+
input.record.purpose,
|
|
911
|
+
"issue",
|
|
912
|
+
input.record.requestId,
|
|
913
|
+
scopes,
|
|
914
|
+
now,
|
|
915
|
+
retentionUntil,
|
|
916
|
+
);
|
|
879
917
|
if (activeProofId !== null && activeProofId !== undefined)
|
|
880
918
|
yield* transaction
|
|
881
919
|
.update(mapping.generation.table)
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import type { Effect } from "effect";
|
|
1
2
|
import type { Statement } from "effect/sql/Statement";
|
|
2
3
|
|
|
4
|
+
import type { PersistenceMappingError } from "./mapping-error";
|
|
5
|
+
import type { NativeDatabase } from "./transaction-kernel";
|
|
6
|
+
|
|
3
7
|
/** Adapter-owned table metadata. No driver or ORM type enters the shared models. */
|
|
4
8
|
export interface TableModel {
|
|
5
9
|
readonly table: object;
|
|
@@ -47,6 +51,10 @@ export interface QueryOperations<
|
|
|
47
51
|
Fragment extends SqlFragment = SqlFragment,
|
|
48
52
|
Column extends SqlColumn = SqlColumn,
|
|
49
53
|
> {
|
|
54
|
+
/** Acquisition-only validation; adapters provide their captured root database. */
|
|
55
|
+
readonly validateStorage?: (
|
|
56
|
+
mapping: unknown,
|
|
57
|
+
) => Effect.Effect<void, PersistenceMappingError, NativeDatabase>;
|
|
50
58
|
readonly and: Predicate<Fragment>;
|
|
51
59
|
readonly or: Predicate<Fragment>;
|
|
52
60
|
readonly balancedD1And: Predicate<Fragment>;
|
|
@@ -61,6 +61,7 @@ import {
|
|
|
61
61
|
validateStepUpPlan,
|
|
62
62
|
stepUpRotationMatches,
|
|
63
63
|
} from "./step-up-state";
|
|
64
|
+
import type { NativeDatabase } from "./transaction-kernel";
|
|
64
65
|
|
|
65
66
|
type CommitMode = "interactive" | "synchronous";
|
|
66
67
|
|
|
@@ -109,6 +110,7 @@ export class CurrentSessionSql extends Context.Service<CurrentSessionSql, Sessio
|
|
|
109
110
|
type Guard = Effect.Effect<void, SessionUnavailable>;
|
|
110
111
|
|
|
111
112
|
export interface SessionSqlOptions {
|
|
113
|
+
readonly coordinated?: boolean;
|
|
112
114
|
readonly mode: CommitMode;
|
|
113
115
|
readonly locking: boolean;
|
|
114
116
|
/** Target-specific ambient transaction preflight. It runs before IDs or writes. */
|
|
@@ -630,10 +632,15 @@ export const makeSessionKernel = (operations: QueryOperations) => {
|
|
|
630
632
|
options: SessionSqlOptions,
|
|
631
633
|
): Effect.fn.Return<
|
|
632
634
|
AuthenticationAuthority["Service"],
|
|
633
|
-
|
|
634
|
-
LifecycleHooks | CurrentSessionSql
|
|
635
|
+
SessionUnavailable,
|
|
636
|
+
LifecycleHooks | CurrentSessionSql | NativeDatabase
|
|
635
637
|
> {
|
|
636
638
|
const database = yield* CurrentSessionSql;
|
|
639
|
+
|
|
640
|
+
if (!options.coordinated)
|
|
641
|
+
yield* (operations.validateStorage?.(mapping) ?? Effect.void).pipe(
|
|
642
|
+
Effect.mapError(unavailable),
|
|
643
|
+
);
|
|
637
644
|
const hooks = yield* LifecycleHooks;
|
|
638
645
|
|
|
639
646
|
const service = {
|
|
@@ -790,8 +797,17 @@ export const makeSessionKernel = (operations: QueryOperations) => {
|
|
|
790
797
|
const makeSqlPendingAuthentication = Effect.fnUntraced(function* <Claims>(
|
|
791
798
|
mapping: PendingAuthenticationMapping<Claims, any, any, any, any, any>,
|
|
792
799
|
options: SessionSqlOptions,
|
|
793
|
-
): Effect.fn.Return<
|
|
800
|
+
): Effect.fn.Return<
|
|
801
|
+
PendingAuthentication<Claims>,
|
|
802
|
+
SessionUnavailable,
|
|
803
|
+
LifecycleHooks | CurrentSessionSql | NativeDatabase
|
|
804
|
+
> {
|
|
794
805
|
const database = yield* CurrentSessionSql;
|
|
806
|
+
|
|
807
|
+
if (!options.coordinated)
|
|
808
|
+
yield* (operations.validateStorage?.(mapping) ?? Effect.void).pipe(
|
|
809
|
+
Effect.mapError(unavailable),
|
|
810
|
+
);
|
|
795
811
|
const hooks = yield* LifecycleHooks;
|
|
796
812
|
|
|
797
813
|
const service = {
|
|
@@ -964,10 +980,15 @@ export const makeSessionKernel = (operations: QueryOperations) => {
|
|
|
964
980
|
readonly statefulSessionPersistence: StatefulSessionPersistence<Claims>;
|
|
965
981
|
readonly sessionRepository: SessionRepository;
|
|
966
982
|
},
|
|
967
|
-
|
|
968
|
-
LifecycleHooks | CurrentSessionSql
|
|
983
|
+
SessionUnavailable,
|
|
984
|
+
LifecycleHooks | CurrentSessionSql | NativeDatabase
|
|
969
985
|
> {
|
|
970
986
|
const database = yield* CurrentSessionSql;
|
|
987
|
+
|
|
988
|
+
if (!options.coordinated)
|
|
989
|
+
yield* (operations.validateStorage?.(mapping) ?? Effect.void).pipe(
|
|
990
|
+
Effect.mapError(unavailable),
|
|
991
|
+
);
|
|
971
992
|
const hooks = yield* LifecycleHooks;
|
|
972
993
|
|
|
973
994
|
const c = sessionColumns(mapping);
|
|
@@ -1018,6 +1039,8 @@ export const makeSessionKernel = (operations: QueryOperations) => {
|
|
|
1018
1039
|
|
|
1019
1040
|
const sessionId = yield* mapping.sessionId.toSession(nativeSessionId);
|
|
1020
1041
|
|
|
1042
|
+
if (sessionId === input.handoffSourceSessionId) return yield* SessionConflict.make({});
|
|
1043
|
+
|
|
1021
1044
|
const version = yield* allocate(
|
|
1022
1045
|
options.mode,
|
|
1023
1046
|
mapping.session.allocateVersion,
|
|
@@ -1043,7 +1066,10 @@ export const makeSessionKernel = (operations: QueryOperations) => {
|
|
|
1043
1066
|
version,
|
|
1044
1067
|
subjectId: input.evidence.revision.subjectId,
|
|
1045
1068
|
securityRevision: input.evidence.revision.securityRevision,
|
|
1046
|
-
assurance:
|
|
1069
|
+
assurance:
|
|
1070
|
+
input.handoffSourceSessionId === undefined
|
|
1071
|
+
? assessed.assurance
|
|
1072
|
+
: input.session.assurance,
|
|
1047
1073
|
provenance: yield* snapshotSessionAuthenticationProvenance({
|
|
1048
1074
|
evidence: input.evidence,
|
|
1049
1075
|
}),
|
|
@@ -1067,6 +1093,17 @@ export const makeSessionKernel = (operations: QueryOperations) => {
|
|
|
1067
1093
|
}),
|
|
1068
1094
|
);
|
|
1069
1095
|
|
|
1096
|
+
// Writes can suspend after prepare. Recheck the capped lifetime and
|
|
1097
|
+
// original proof freshness while the authority rows remain locked.
|
|
1098
|
+
const commitNow = yield* freshNow;
|
|
1099
|
+
|
|
1100
|
+
if (
|
|
1101
|
+
DateTime.toEpochMillis(commitNow) < DateTime.toEpochMillis(record.issuedAt) ||
|
|
1102
|
+
DateTime.toEpochMillis(commitNow) >= DateTime.toEpochMillis(record.expiresAt) ||
|
|
1103
|
+
!(yield* assessAuthentication(input.evidence, requirement)).satisfied
|
|
1104
|
+
)
|
|
1105
|
+
return yield* stale();
|
|
1106
|
+
|
|
1070
1107
|
return receipt;
|
|
1071
1108
|
}).pipe(normalizeMutation(mapping)),
|
|
1072
1109
|
),
|
|
@@ -1388,8 +1425,17 @@ export const makeSessionKernel = (operations: QueryOperations) => {
|
|
|
1388
1425
|
const makeSqlSignedValidity = Effect.fnUntraced(function* (
|
|
1389
1426
|
mapping: SignedSessionValidityMapping<any, any, any, any>,
|
|
1390
1427
|
options: SessionSqlOptions,
|
|
1391
|
-
): Effect.fn.Return<
|
|
1428
|
+
): Effect.fn.Return<
|
|
1429
|
+
SignedSessionValidity,
|
|
1430
|
+
SessionUnavailable,
|
|
1431
|
+
LifecycleHooks | CurrentSessionSql | NativeDatabase
|
|
1432
|
+
> {
|
|
1392
1433
|
const database = yield* CurrentSessionSql;
|
|
1434
|
+
|
|
1435
|
+
if (!options.coordinated)
|
|
1436
|
+
yield* (operations.validateStorage?.(mapping) ?? Effect.void).pipe(
|
|
1437
|
+
Effect.mapError(unavailable),
|
|
1438
|
+
);
|
|
1393
1439
|
const hooks = yield* LifecycleHooks;
|
|
1394
1440
|
|
|
1395
1441
|
const subject = subjectColumns(mapping);
|
|
@@ -1780,8 +1826,17 @@ export const makeSessionKernel = (operations: QueryOperations) => {
|
|
|
1780
1826
|
const makeSqlSessionStepUp = Effect.fnUntraced(function* <Claims>(
|
|
1781
1827
|
mapping: SessionStepUpMapping<Claims, any, any, any, any, any, any, any>,
|
|
1782
1828
|
options: SessionSqlOptions,
|
|
1783
|
-
): Effect.fn.Return<
|
|
1829
|
+
): Effect.fn.Return<
|
|
1830
|
+
SessionStepUpPersistence<Claims>,
|
|
1831
|
+
SessionUnavailable,
|
|
1832
|
+
LifecycleHooks | CurrentSessionSql | NativeDatabase
|
|
1833
|
+
> {
|
|
1784
1834
|
const database = yield* CurrentSessionSql;
|
|
1835
|
+
|
|
1836
|
+
if (!options.coordinated)
|
|
1837
|
+
yield* (operations.validateStorage?.(mapping) ?? Effect.void).pipe(
|
|
1838
|
+
Effect.mapError(unavailable),
|
|
1839
|
+
);
|
|
1785
1840
|
const hooks = yield* LifecycleHooks;
|
|
1786
1841
|
|
|
1787
1842
|
const m = mapping.intent,
|
|
@@ -76,6 +76,11 @@ const ProofContinuation = Schema.Struct({
|
|
|
76
76
|
version: ProofVersion,
|
|
77
77
|
});
|
|
78
78
|
|
|
79
|
+
/** Derive shared row mappings from a managed or custom storage layout.
|
|
80
|
+
* Each factory requires its role tables to exist. Adapter authors supply typed
|
|
81
|
+
* table handles and dialect-specific clocks/commit predicates, and may refine
|
|
82
|
+
* authority policy for explicit services. This does not alter composed defaults.
|
|
83
|
+
*/
|
|
79
84
|
export const makeMappings = (input: MappingInput) => {
|
|
80
85
|
const table = (role: StorageRole) => {
|
|
81
86
|
const found = input.tables[role];
|
|
@@ -11,6 +11,13 @@ const PgColumns = Schema.Array(Schema.Struct({ name: Schema.String }));
|
|
|
11
11
|
const PgKeys = Schema.Array(Schema.Struct({ columns: Schema.Array(Schema.String) }));
|
|
12
12
|
const quote = (value: string) => '"' + value.replaceAll('"', '""') + '"';
|
|
13
13
|
|
|
14
|
+
/** Physical metadata needed for catalog validation; value codecs remain adapter-owned. */
|
|
15
|
+
export interface PhysicalStorageTable {
|
|
16
|
+
readonly name: string;
|
|
17
|
+
readonly schema?: string;
|
|
18
|
+
readonly columns: Readonly<Record<string, { readonly name: string }>>;
|
|
19
|
+
}
|
|
20
|
+
|
|
14
21
|
export const qualifiedTableName = (table: Pick<StorageTable, "name" | "schema">) =>
|
|
15
22
|
(table.schema === undefined ? "" : quote(table.schema) + ".") + quote(table.name);
|
|
16
23
|
|
|
@@ -18,8 +25,8 @@ export const qualifiedTableName = (table: Pick<StorageTable, "name" | "schema">)
|
|
|
18
25
|
* partial or expression index cannot establish these unconditional guarantees. */
|
|
19
26
|
export const validateStorage = Effect.fnUntraced(
|
|
20
27
|
function* (
|
|
21
|
-
dialect: "pg" | "sqlite",
|
|
22
|
-
table:
|
|
28
|
+
dialect: "pg" | "mysql" | "sqlite",
|
|
29
|
+
table: PhysicalStorageTable,
|
|
23
30
|
required: ReadonlyArray<ReadonlyArray<string>>,
|
|
24
31
|
) {
|
|
25
32
|
const client = yield* SqlClient;
|
|
@@ -64,22 +71,76 @@ export const validateStorage = Effect.fnUntraced(
|
|
|
64
71
|
|
|
65
72
|
return keys;
|
|
66
73
|
})
|
|
67
|
-
:
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
74
|
+
: dialect === "mysql"
|
|
75
|
+
? Effect.gen(function* () {
|
|
76
|
+
const columns = yield* client`
|
|
77
|
+
select column_name as name from information_schema.columns
|
|
78
|
+
where table_schema = coalesce(${table.schema ?? null}, database())
|
|
79
|
+
and table_name = ${table.name}
|
|
80
|
+
`.pipe(Effect.flatMap(Schema.decodeUnknownEffect(PgColumns)));
|
|
81
|
+
|
|
82
|
+
for (const column of Object.values(table.columns)) {
|
|
83
|
+
if (!columns.some((actual) => actual.name === column.name))
|
|
84
|
+
return yield* PersistenceConfigurationError.make({
|
|
85
|
+
reason: `Missing SQL column ${table.name}.${column.name}`,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const indexes = yield* client`
|
|
90
|
+
select index_name as name, column_name as columnName, sub_part as prefix
|
|
91
|
+
from information_schema.statistics
|
|
92
|
+
where table_schema = coalesce(${table.schema ?? null}, database())
|
|
93
|
+
and table_name = ${table.name} and non_unique = 0
|
|
94
|
+
order by index_name, seq_in_index
|
|
95
|
+
`.pipe(
|
|
96
|
+
Effect.flatMap(
|
|
97
|
+
Schema.decodeUnknownEffect(
|
|
98
|
+
Schema.Array(
|
|
99
|
+
Schema.Struct({
|
|
100
|
+
name: Schema.String,
|
|
101
|
+
columnName: Schema.NullOr(Schema.String),
|
|
102
|
+
prefix: Schema.NullOr(Schema.Number),
|
|
103
|
+
}),
|
|
104
|
+
),
|
|
105
|
+
),
|
|
106
|
+
),
|
|
73
107
|
);
|
|
74
108
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
109
|
+
const keys = new Map<string, Array<(typeof indexes)[number]>>();
|
|
110
|
+
|
|
111
|
+
for (const index of indexes) {
|
|
112
|
+
const columns = keys.get(index.name) ?? [];
|
|
113
|
+
|
|
114
|
+
columns.push(index);
|
|
115
|
+
keys.set(index.name, columns);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return [...keys.values()]
|
|
119
|
+
.filter((columns) =>
|
|
120
|
+
columns.every((column) => column.columnName !== null && column.prefix === null),
|
|
121
|
+
)
|
|
122
|
+
.map((columns) =>
|
|
123
|
+
columns.flatMap((column) =>
|
|
124
|
+
column.columnName === null ? [] : [column.columnName],
|
|
125
|
+
),
|
|
126
|
+
);
|
|
127
|
+
})
|
|
128
|
+
: Effect.gen(function* () {
|
|
129
|
+
const relation = qualifiedTableName(table);
|
|
130
|
+
|
|
131
|
+
const columns =
|
|
132
|
+
yield* client`select attname as name from pg_attribute where attrelid = to_regclass(${relation}) and attnum > 0 and not attisdropped`.pipe(
|
|
133
|
+
Effect.flatMap(Schema.decodeUnknownEffect(PgColumns)),
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
for (const column of Object.values(table.columns)) {
|
|
137
|
+
if (!columns.some((actual) => actual.name === column.name))
|
|
138
|
+
return yield* PersistenceConfigurationError.make({
|
|
139
|
+
reason: `Missing SQL column ${relation}.${column.name}`,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const indexes = yield* client`
|
|
83
144
|
select array_agg(a.attname order by k.ordinality) as columns
|
|
84
145
|
from pg_index i
|
|
85
146
|
cross join lateral unnest(i.indkey) with ordinality as k(attnum, ordinality)
|
|
@@ -90,8 +151,8 @@ export const validateStorage = Effect.fnUntraced(
|
|
|
90
151
|
group by i.indexrelid
|
|
91
152
|
`.pipe(Effect.flatMap(Schema.decodeUnknownEffect(PgKeys)));
|
|
92
153
|
|
|
93
|
-
|
|
94
|
-
|
|
154
|
+
return indexes.map((index) => index.columns);
|
|
155
|
+
});
|
|
95
156
|
|
|
96
157
|
for (const keys of required) {
|
|
97
158
|
const columns = keys.map((key) => table.columns[key]?.name);
|
|
@@ -99,8 +160,11 @@ export const validateStorage = Effect.fnUntraced(
|
|
|
99
160
|
if (
|
|
100
161
|
columns.some((column) => column === undefined) ||
|
|
101
162
|
!physicalKeys.some(
|
|
163
|
+
// A stronger key also guarantees the required tuple's uniqueness.
|
|
102
164
|
(actual) =>
|
|
103
|
-
actual.length
|
|
165
|
+
actual.length > 0 &&
|
|
166
|
+
actual.length <= columns.length &&
|
|
167
|
+
actual.every((column) => columns.includes(column)),
|
|
104
168
|
)
|
|
105
169
|
)
|
|
106
170
|
return yield* PersistenceConfigurationError.make({
|
|
@@ -203,6 +203,7 @@ export const makeTransactionExecutionKernel = (
|
|
|
203
203
|
OwnerId,
|
|
204
204
|
Resources,
|
|
205
205
|
AllocationError,
|
|
206
|
+
AllocationRequirements,
|
|
206
207
|
Services,
|
|
207
208
|
Transaction,
|
|
208
209
|
A,
|
|
@@ -212,7 +213,7 @@ export const makeTransactionExecutionKernel = (
|
|
|
212
213
|
database: TransactionNativeDatabase,
|
|
213
214
|
ownerTag: Context.Key<OwnerId, TransactionOwner<Failure>>,
|
|
214
215
|
configuration: TransactionTargetConfiguration<Failure>,
|
|
215
|
-
allocate: Effect.Effect<Resources, AllocationError>,
|
|
216
|
+
allocate: Effect.Effect<Resources, AllocationError, AllocationRequirements>,
|
|
216
217
|
unavailable: () => Failure,
|
|
217
218
|
nonce: () => string,
|
|
218
219
|
services: (execution: TransactionExecution<Failure, OwnerId>, resources: Resources) => Services,
|
|
@@ -224,7 +225,7 @@ export const makeTransactionExecutionKernel = (
|
|
|
224
225
|
): Effect.Effect<
|
|
225
226
|
A,
|
|
226
227
|
TransactionCoordinatorError<E, Failure>,
|
|
227
|
-
Exclude<R, CurrentCommitJournal> | LifecycleHooks
|
|
228
|
+
Exclude<R, CurrentCommitJournal> | LifecycleHooks | AllocationRequirements
|
|
228
229
|
> => {
|
|
229
230
|
return Effect.gen(function* () {
|
|
230
231
|
if (yield* hasCommitScope) return yield* Effect.fail(unavailable());
|
|
@@ -31,7 +31,7 @@ export interface TransactionNativeDatabase {
|
|
|
31
31
|
) => Effect.Effect<A, E | SqlError, R>;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
/**
|
|
34
|
+
/** Captured root database for service construction and pre-transaction catalog checks.
|
|
35
35
|
* Active transactions remain owned by their coordinator and journal. */
|
|
36
36
|
export class NativeDatabase extends Context.Service<NativeDatabase, TransactionNativeDatabase>()(
|
|
37
37
|
"effect-auth/persistence/NativeDatabase",
|