import * as Context from "effect/Context";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as Schema from "effect/Schema";import * as SqlClient from "effect/unstable/sql/SqlClient";import * as SqlSchema from "effect/unstable/sql/SqlSchema"; import { AuthClientMetadataDeviceType, AuthEnvironmentScopes, AuthSessionId, ServerAuthSessionMethod,} from "@t3tools/contracts"; import { type AuthSessionRepositoryError, PersistenceDecodeError, type PersistenceErrorCorrelation, PersistenceSqlError,} from "./Errors.ts"; export const AuthSessionClientMetadataRecord = Schema.Struct({ label: Schema.NullOr(Schema.String), ipAddress: Schema.NullOr(Schema.String), userAgent: Schema.NullOr(Schema.String), deviceType: AuthClientMetadataDeviceType, os: Schema.NullOr(Schema.String), browser: Schema.NullOr(Schema.String),});export type AuthSessionClientMetadataRecord = typeof AuthSessionClientMetadataRecord.Type; export const AuthSessionRecord = Schema.Struct({ sessionId: AuthSessionId, subject: Schema.String, scopes: AuthEnvironmentScopes, method: ServerAuthSessionMethod, client: AuthSessionClientMetadataRecord, issuedAt: Schema.DateTimeUtcFromString, expiresAt: Schema.DateTimeUtcFromString, lastConnectedAt: Schema.NullOr(Schema.DateTimeUtcFromString), revokedAt: Schema.NullOr(Schema.DateTimeUtcFromString),});export type AuthSessionRecord = typeof AuthSessionRecord.Type; export const CreateAuthSessionInput = Schema.Struct({ sessionId: AuthSessionId, subject: Schema.String, scopes: AuthEnvironmentScopes, method: ServerAuthSessionMethod, client: AuthSessionClientMetadataRecord, issuedAt: Schema.DateTimeUtcFromString, expiresAt: Schema.DateTimeUtcFromString,});export type CreateAuthSessionInput = typeof CreateAuthSessionInput.Type; export const GetAuthSessionByIdInput = Schema.Struct({ sessionId: AuthSessionId,});export type GetAuthSessionByIdInput = typeof GetAuthSessionByIdInput.Type; export const ListActiveAuthSessionsInput = Schema.Struct({ now: Schema.DateTimeUtcFromString,});export type ListActiveAuthSessionsInput = typeof ListActiveAuthSessionsInput.Type; export const RevokeAuthSessionInput = Schema.Struct({ sessionId: AuthSessionId, revokedAt: Schema.DateTimeUtcFromString,});export type RevokeAuthSessionInput = typeof RevokeAuthSessionInput.Type; export const RevokeOtherAuthSessionsInput = Schema.Struct({ currentSessionId: AuthSessionId, revokedAt: Schema.DateTimeUtcFromString,});export type RevokeOtherAuthSessionsInput = typeof RevokeOtherAuthSessionsInput.Type; export const SetAuthSessionLastConnectedAtInput = Schema.Struct({ sessionId: AuthSessionId, lastConnectedAt: Schema.DateTimeUtcFromString,});export type SetAuthSessionLastConnectedAtInput = typeof SetAuthSessionLastConnectedAtInput.Type; export class AuthSessionRepository extends Context.Service< AuthSessionRepository, { readonly create: ( input: CreateAuthSessionInput, ) => Effect.Effect<void, AuthSessionRepositoryError>; readonly getById: ( input: GetAuthSessionByIdInput, ) => Effect.Effect<Option.Option<AuthSessionRecord>, AuthSessionRepositoryError>; readonly listActive: ( input: ListActiveAuthSessionsInput, ) => Effect.Effect<ReadonlyArray<AuthSessionRecord>, AuthSessionRepositoryError>; readonly revoke: ( input: RevokeAuthSessionInput, ) => Effect.Effect<boolean, AuthSessionRepositoryError>; readonly revokeAllExcept: ( input: RevokeOtherAuthSessionsInput, ) => Effect.Effect<ReadonlyArray<AuthSessionId>, AuthSessionRepositoryError>; readonly setLastConnectedAt: ( input: SetAuthSessionLastConnectedAtInput, ) => Effect.Effect<void, AuthSessionRepositoryError>; }>()("t3/persistence/AuthSessions/AuthSessionRepository") {} const AuthSessionDbRow = Schema.Struct({ sessionId: AuthSessionId, subject: Schema.String, scopes: Schema.fromJsonString(AuthEnvironmentScopes), method: ServerAuthSessionMethod, clientLabel: Schema.NullOr(Schema.String), clientIpAddress: Schema.NullOr(Schema.String), clientUserAgent: Schema.NullOr(Schema.String), clientDeviceType: Schema.Literals(["desktop", "mobile", "tablet", "bot", "unknown"]), clientOs: Schema.NullOr(Schema.String), clientBrowser: Schema.NullOr(Schema.String), issuedAt: Schema.DateTimeUtcFromString, expiresAt: Schema.DateTimeUtcFromString, lastConnectedAt: Schema.NullOr(Schema.DateTimeUtcFromString), revokedAt: Schema.NullOr(Schema.DateTimeUtcFromString),}); const AuthSessionRawDbRow = Schema.Struct({ sessionId: Schema.String, subject: Schema.Unknown, scopes: Schema.Unknown, method: Schema.Unknown, clientLabel: Schema.Unknown, clientIpAddress: Schema.Unknown, clientUserAgent: Schema.Unknown, clientDeviceType: Schema.Unknown, clientOs: Schema.Unknown, clientBrowser: Schema.Unknown, issuedAt: Schema.Unknown, expiresAt: Schema.Unknown, lastConnectedAt: Schema.Unknown, revokedAt: Schema.Unknown,}); const decodeAuthSessionDbRow = Schema.decodeUnknownEffect(AuthSessionDbRow); function toAuthSessionRecord(row: typeof AuthSessionDbRow.Type): AuthSessionRecord { return { sessionId: row.sessionId, subject: row.subject, scopes: row.scopes, method: row.method, client: { label: row.clientLabel, ipAddress: row.clientIpAddress, userAgent: row.clientUserAgent, deviceType: row.clientDeviceType, os: row.clientOs, browser: row.clientBrowser, }, issuedAt: row.issuedAt, expiresAt: row.expiresAt, lastConnectedAt: row.lastConnectedAt, revokedAt: row.revokedAt, };} function toPersistenceSqlOrDecodeError( sqlOperation: string, decodeOperation: string, correlation?: PersistenceErrorCorrelation,) { return (cause: unknown): AuthSessionRepositoryError => Schema.isSchemaError(cause) ? PersistenceDecodeError.fromSchemaError(decodeOperation, cause, correlation) : new PersistenceSqlError({ operation: sqlOperation, ...(correlation === undefined ? {} : { correlation }), cause, });} export const make = Effect.gen(function* () { const sql = yield* SqlClient.SqlClient; const createSessionRow = SqlSchema.void({ Request: CreateAuthSessionInput, execute: (input) => sql` INSERT INTO auth_sessions ( session_id, subject, scopes, method, client_label, client_ip_address, client_user_agent, client_device_type, client_os, client_browser, issued_at, expires_at, revoked_at ) VALUES ( ${input.sessionId}, ${input.subject}, ${JSON.stringify(input.scopes)}, ${input.method}, ${input.client.label}, ${input.client.ipAddress}, ${input.client.userAgent}, ${input.client.deviceType}, ${input.client.os}, ${input.client.browser}, ${input.issuedAt}, ${input.expiresAt}, NULL ) `, }); const getSessionRowById = SqlSchema.findOneOption({ Request: GetAuthSessionByIdInput, Result: AuthSessionRawDbRow, execute: ({ sessionId }) => sql` SELECT session_id AS "sessionId", subject AS "subject", scopes AS "scopes", method AS "method", client_label AS "clientLabel", client_ip_address AS "clientIpAddress", client_user_agent AS "clientUserAgent", client_device_type AS "clientDeviceType", client_os AS "clientOs", client_browser AS "clientBrowser", issued_at AS "issuedAt", expires_at AS "expiresAt", last_connected_at AS "lastConnectedAt", revoked_at AS "revokedAt" FROM auth_sessions WHERE session_id = ${sessionId} `, }); const listActiveSessionRows = SqlSchema.findAll({ Request: ListActiveAuthSessionsInput, Result: AuthSessionRawDbRow, execute: ({ now }) => sql` SELECT session_id AS "sessionId", subject AS "subject", scopes AS "scopes", method AS "method", client_label AS "clientLabel", client_ip_address AS "clientIpAddress", client_user_agent AS "clientUserAgent", client_device_type AS "clientDeviceType", client_os AS "clientOs", client_browser AS "clientBrowser", issued_at AS "issuedAt", expires_at AS "expiresAt", last_connected_at AS "lastConnectedAt", revoked_at AS "revokedAt" FROM auth_sessions WHERE revoked_at IS NULL AND expires_at > ${now} ORDER BY issued_at DESC, session_id DESC `, }); const setLastConnectedAtRow = SqlSchema.void({ Request: SetAuthSessionLastConnectedAtInput, execute: ({ sessionId, lastConnectedAt }) => sql` UPDATE auth_sessions SET last_connected_at = ${lastConnectedAt} WHERE session_id = ${sessionId} AND revoked_at IS NULL `, }); const revokeSessionRows = SqlSchema.findAll({ Request: RevokeAuthSessionInput, Result: Schema.Struct({ sessionId: AuthSessionId }), execute: ({ sessionId, revokedAt }) => sql` UPDATE auth_sessions SET revoked_at = ${revokedAt} WHERE session_id = ${sessionId} AND revoked_at IS NULL RETURNING session_id AS "sessionId" `, }); const revokeOtherSessionRows = SqlSchema.findAll({ Request: RevokeOtherAuthSessionsInput, Result: Schema.Struct({ sessionId: AuthSessionId }), execute: ({ currentSessionId, revokedAt }) => sql` UPDATE auth_sessions SET revoked_at = ${revokedAt} WHERE session_id <> ${currentSessionId} AND revoked_at IS NULL RETURNING session_id AS "sessionId" `, }); const create: AuthSessionRepository["Service"]["create"] = (input) => createSessionRow(input).pipe( Effect.mapError( toPersistenceSqlOrDecodeError( "AuthSessionRepository.create:query", "AuthSessionRepository.create:encodeRequest", { sessionId: input.sessionId }, ), ), ); const getById: AuthSessionRepository["Service"]["getById"] = (input) => getSessionRowById(input).pipe( Effect.mapError( toPersistenceSqlOrDecodeError( "AuthSessionRepository.getById:query", "AuthSessionRepository.getById:decodeRow", { sessionId: input.sessionId }, ), ), Effect.flatMap((rowOption) => Option.match(rowOption, { onNone: () => Effect.succeed(Option.none()), onSome: (row) => decodeAuthSessionDbRow(row).pipe( Effect.mapError((cause) => PersistenceDecodeError.fromSchemaError( "AuthSessionRepository.getById:decodeRow", cause, { sessionId: input.sessionId }, ), ), Effect.map((decodedRow) => Option.some(toAuthSessionRecord(decodedRow))), ), }), ), ); const listActive: AuthSessionRepository["Service"]["listActive"] = (input) => listActiveSessionRows(input).pipe( Effect.mapError( toPersistenceSqlOrDecodeError( "AuthSessionRepository.listActive:query", "AuthSessionRepository.listActive:decodeRows", ), ), Effect.flatMap((rows) => Effect.forEach(rows, (row) => decodeAuthSessionDbRow(row).pipe( Effect.mapError((cause) => PersistenceDecodeError.fromSchemaError( "AuthSessionRepository.listActive:decodeRows", cause, { sessionId: row.sessionId }, ), ), Effect.map(toAuthSessionRecord), ), ), ), ); const revoke: AuthSessionRepository["Service"]["revoke"] = (input) => revokeSessionRows(input).pipe( Effect.mapError( toPersistenceSqlOrDecodeError( "AuthSessionRepository.revoke:query", "AuthSessionRepository.revoke:decodeRows", { sessionId: input.sessionId }, ), ), Effect.map((rows) => rows.length > 0), ); const revokeAllExcept: AuthSessionRepository["Service"]["revokeAllExcept"] = (input) => revokeOtherSessionRows(input).pipe( Effect.mapError( toPersistenceSqlOrDecodeError( "AuthSessionRepository.revokeAllExcept:query", "AuthSessionRepository.revokeAllExcept:decodeRows", { currentSessionId: input.currentSessionId }, ), ), Effect.map((rows) => rows.map((row) => row.sessionId)), ); const setLastConnectedAt: AuthSessionRepository["Service"]["setLastConnectedAt"] = (input) => setLastConnectedAtRow(input).pipe( Effect.mapError( toPersistenceSqlOrDecodeError( "AuthSessionRepository.setLastConnectedAt:query", "AuthSessionRepository.setLastConnectedAt:encodeRequest", { sessionId: input.sessionId }, ), ), ); return { create, getById, listActive, revoke, revokeAllExcept, setLastConnectedAt, } satisfies AuthSessionRepository["Service"];}); export const layer = Layer.effect(AuthSessionRepository, make);
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Schema from "effect/Schema";
import * as SqlClient from "effect/unstable/sql/SqlClient";
import * as SqlSchema from "effect/unstable/sql/SqlSchema";
import {
AuthClientMetadataDeviceType,
AuthEnvironmentScopes,
AuthSessionId,
ServerAuthSessionMethod,
} from "@t3tools/contracts";
import {
type AuthSessionRepositoryError,
PersistenceDecodeError,
type PersistenceErrorCorrelation,
PersistenceSqlError,
} from "./Errors.ts";
export const AuthSessionClientMetadataRecord = Schema.Struct({
label: Schema.NullOr(Schema.String),
ipAddress: Schema.NullOr(Schema.String),
userAgent: Schema.NullOr(Schema.String),
deviceType: AuthClientMetadataDeviceType,
os: Schema.NullOr(Schema.String),
browser: Schema.NullOr(Schema.String),
});
export type AuthSessionClientMetadataRecord = typeof AuthSessionClientMetadataRecord.Type;
export const AuthSessionRecord = Schema.Struct({
sessionId: AuthSessionId,
subject: Schema.String,
scopes: AuthEnvironmentScopes,
method: ServerAuthSessionMethod,
client: AuthSessionClientMetadataRecord,
issuedAt: Schema.DateTimeUtcFromString,
expiresAt: Schema.DateTimeUtcFromString,
lastConnectedAt: Schema.NullOr(Schema.DateTimeUtcFromString),
revokedAt: Schema.NullOr(Schema.DateTimeUtcFromString),
});
export type AuthSessionRecord = typeof AuthSessionRecord.Type;
export const CreateAuthSessionInput = Schema.Struct({
sessionId: AuthSessionId,
subject: Schema.String,
scopes: AuthEnvironmentScopes,
method: ServerAuthSessionMethod,
client: AuthSessionClientMetadataRecord,
issuedAt: Schema.DateTimeUtcFromString,
expiresAt: Schema.DateTimeUtcFromString,
});
export type CreateAuthSessionInput = typeof CreateAuthSessionInput.Type;
export const GetAuthSessionByIdInput = Schema.Struct({
sessionId: AuthSessionId,
});
export type GetAuthSessionByIdInput = typeof GetAuthSessionByIdInput.Type;
export const ListActiveAuthSessionsInput = Schema.Struct({
now: Schema.DateTimeUtcFromString,
});
export type ListActiveAuthSessionsInput = typeof ListActiveAuthSessionsInput.Type;
export const RevokeAuthSessionInput = Schema.Struct({
sessionId: AuthSessionId,
revokedAt: Schema.DateTimeUtcFromString,
});
export type RevokeAuthSessionInput = typeof RevokeAuthSessionInput.Type;
export const RevokeOtherAuthSessionsInput = Schema.Struct({
currentSessionId: AuthSessionId,
revokedAt: Schema.DateTimeUtcFromString,
});
export type RevokeOtherAuthSessionsInput = typeof RevokeOtherAuthSessionsInput.Type;
export const SetAuthSessionLastConnectedAtInput = Schema.Struct({
sessionId: AuthSessionId,
lastConnectedAt: Schema.DateTimeUtcFromString,
});
export type SetAuthSessionLastConnectedAtInput = typeof SetAuthSessionLastConnectedAtInput.Type;
export class AuthSessionRepository extends Context.Service<
AuthSessionRepository,
{
readonly create: (
input: CreateAuthSessionInput,
) => Effect.Effect<void, AuthSessionRepositoryError>;
readonly getById: (
input: GetAuthSessionByIdInput,
) => Effect.Effect<Option.Option<AuthSessionRecord>, AuthSessionRepositoryError>;
readonly listActive: (
input: ListActiveAuthSessionsInput,
) => Effect.Effect<ReadonlyArray<AuthSessionRecord>, AuthSessionRepositoryError>;
readonly revoke: (
input: RevokeAuthSessionInput,
) => Effect.Effect<boolean, AuthSessionRepositoryError>;
readonly revokeAllExcept: (
input: RevokeOtherAuthSessionsInput,
) => Effect.Effect<ReadonlyArray<AuthSessionId>, AuthSessionRepositoryError>;
readonly setLastConnectedAt: (
input: SetAuthSessionLastConnectedAtInput,
) => Effect.Effect<void, AuthSessionRepositoryError>;
}
>()("t3/persistence/AuthSessions/AuthSessionRepository") {}
const AuthSessionDbRow = Schema.Struct({
sessionId: AuthSessionId,
subject: Schema.String,
scopes: Schema.fromJsonString(AuthEnvironmentScopes),
method: ServerAuthSessionMethod,
clientLabel: Schema.NullOr(Schema.String),
clientIpAddress: Schema.NullOr(Schema.String),
clientUserAgent: Schema.NullOr(Schema.String),
clientDeviceType: Schema.Literals(["desktop", "mobile", "tablet", "bot", "unknown"]),
clientOs: Schema.NullOr(Schema.String),
clientBrowser: Schema.NullOr(Schema.String),
issuedAt: Schema.DateTimeUtcFromString,
expiresAt: Schema.DateTimeUtcFromString,
lastConnectedAt: Schema.NullOr(Schema.DateTimeUtcFromString),
revokedAt: Schema.NullOr(Schema.DateTimeUtcFromString),
});
const AuthSessionRawDbRow = Schema.Struct({
sessionId: Schema.String,
subject: Schema.Unknown,
scopes: Schema.Unknown,
method: Schema.Unknown,
clientLabel: Schema.Unknown,
clientIpAddress: Schema.Unknown,
clientUserAgent: Schema.Unknown,
clientDeviceType: Schema.Unknown,
clientOs: Schema.Unknown,
clientBrowser: Schema.Unknown,
issuedAt: Schema.Unknown,
expiresAt: Schema.Unknown,
lastConnectedAt: Schema.Unknown,
revokedAt: Schema.Unknown,
});
const decodeAuthSessionDbRow = Schema.decodeUnknownEffect(AuthSessionDbRow);
function toAuthSessionRecord(row: typeof AuthSessionDbRow.Type): AuthSessionRecord {
return {
sessionId: row.sessionId,
subject: row.subject,
scopes: row.scopes,
method: row.method,
client: {
label: row.clientLabel,
ipAddress: row.clientIpAddress,
userAgent: row.clientUserAgent,
deviceType: row.clientDeviceType,
os: row.clientOs,
browser: row.clientBrowser,
},
issuedAt: row.issuedAt,
expiresAt: row.expiresAt,
lastConnectedAt: row.lastConnectedAt,
revokedAt: row.revokedAt,
};
}
function toPersistenceSqlOrDecodeError(
sqlOperation: string,
decodeOperation: string,
correlation?: PersistenceErrorCorrelation,
) {
return (cause: unknown): AuthSessionRepositoryError =>
Schema.isSchemaError(cause)
? PersistenceDecodeError.fromSchemaError(decodeOperation, cause, correlation)
: new PersistenceSqlError({
operation: sqlOperation,
...(correlation === undefined ? {} : { correlation }),
cause,
});
}
export const make = Effect.gen(function* () {
const sql = yield* SqlClient.SqlClient;
const createSessionRow = SqlSchema.void({
Request: CreateAuthSessionInput,
execute: (input) =>
sql`
INSERT INTO auth_sessions (
session_id,
subject,
scopes,
method,
client_label,
client_ip_address,
client_user_agent,
client_device_type,
client_os,
client_browser,
issued_at,
expires_at,
revoked_at
)
VALUES (
${input.sessionId},
${input.subject},
${JSON.stringify(input.scopes)},
${input.method},
${input.client.label},
${input.client.ipAddress},
${input.client.userAgent},
${input.client.deviceType},
${input.client.os},
${input.client.browser},
${input.issuedAt},
${input.expiresAt},
NULL
)
`,
});
const getSessionRowById = SqlSchema.findOneOption({
Request: GetAuthSessionByIdInput,
Result: AuthSessionRawDbRow,
execute: ({ sessionId }) =>
sql`
SELECT
session_id AS "sessionId",
subject AS "subject",
scopes AS "scopes",
method AS "method",
client_label AS "clientLabel",
client_ip_address AS "clientIpAddress",
client_user_agent AS "clientUserAgent",
client_device_type AS "clientDeviceType",
client_os AS "clientOs",
client_browser AS "clientBrowser",
issued_at AS "issuedAt",
expires_at AS "expiresAt",
last_connected_at AS "lastConnectedAt",
revoked_at AS "revokedAt"
FROM auth_sessions
WHERE session_id = ${sessionId}
`,
});
const listActiveSessionRows = SqlSchema.findAll({
Request: ListActiveAuthSessionsInput,
Result: AuthSessionRawDbRow,
execute: ({ now }) =>
sql`
SELECT
session_id AS "sessionId",
subject AS "subject",
scopes AS "scopes",
method AS "method",
client_label AS "clientLabel",
client_ip_address AS "clientIpAddress",
client_user_agent AS "clientUserAgent",
client_device_type AS "clientDeviceType",
client_os AS "clientOs",
client_browser AS "clientBrowser",
issued_at AS "issuedAt",
expires_at AS "expiresAt",
last_connected_at AS "lastConnectedAt",
revoked_at AS "revokedAt"
FROM auth_sessions
WHERE revoked_at IS NULL
AND expires_at > ${now}
ORDER BY issued_at DESC, session_id DESC
`,
});
const setLastConnectedAtRow = SqlSchema.void({
Request: SetAuthSessionLastConnectedAtInput,
execute: ({ sessionId, lastConnectedAt }) =>
sql`
UPDATE auth_sessions
SET last_connected_at = ${lastConnectedAt}
WHERE session_id = ${sessionId}
AND revoked_at IS NULL
`,
});
const revokeSessionRows = SqlSchema.findAll({
Request: RevokeAuthSessionInput,
Result: Schema.Struct({ sessionId: AuthSessionId }),
execute: ({ sessionId, revokedAt }) =>
sql`
UPDATE auth_sessions
SET revoked_at = ${revokedAt}
WHERE session_id = ${sessionId}
AND revoked_at IS NULL
RETURNING session_id AS "sessionId"
`,
});
const revokeOtherSessionRows = SqlSchema.findAll({
Request: RevokeOtherAuthSessionsInput,
Result: Schema.Struct({ sessionId: AuthSessionId }),
execute: ({ currentSessionId, revokedAt }) =>
sql`
UPDATE auth_sessions
SET revoked_at = ${revokedAt}
WHERE session_id <> ${currentSessionId}
AND revoked_at IS NULL
RETURNING session_id AS "sessionId"
`,
});
const create: AuthSessionRepository["Service"]["create"] = (input) =>
createSessionRow(input).pipe(
Effect.mapError(
toPersistenceSqlOrDecodeError(
"AuthSessionRepository.create:query",
"AuthSessionRepository.create:encodeRequest",
{ sessionId: input.sessionId },
),
),
);
const getById: AuthSessionRepository["Service"]["getById"] = (input) =>
getSessionRowById(input).pipe(
Effect.mapError(
toPersistenceSqlOrDecodeError(
"AuthSessionRepository.getById:query",
"AuthSessionRepository.getById:decodeRow",
{ sessionId: input.sessionId },
),
),
Effect.flatMap((rowOption) =>
Option.match(rowOption, {
onNone: () => Effect.succeed(Option.none()),
onSome: (row) =>
decodeAuthSessionDbRow(row).pipe(
Effect.mapError((cause) =>
PersistenceDecodeError.fromSchemaError(
"AuthSessionRepository.getById:decodeRow",
cause,
{ sessionId: input.sessionId },
),
),
Effect.map((decodedRow) => Option.some(toAuthSessionRecord(decodedRow))),
),
}),
),
);
const listActive: AuthSessionRepository["Service"]["listActive"] = (input) =>
listActiveSessionRows(input).pipe(
Effect.mapError(
toPersistenceSqlOrDecodeError(
"AuthSessionRepository.listActive:query",
"AuthSessionRepository.listActive:decodeRows",
),
),
Effect.flatMap((rows) =>
Effect.forEach(rows, (row) =>
decodeAuthSessionDbRow(row).pipe(
Effect.mapError((cause) =>
PersistenceDecodeError.fromSchemaError(
"AuthSessionRepository.listActive:decodeRows",
cause,
{ sessionId: row.sessionId },
),
),
Effect.map(toAuthSessionRecord),
),
),
),
);
const revoke: AuthSessionRepository["Service"]["revoke"] = (input) =>
revokeSessionRows(input).pipe(
Effect.mapError(
toPersistenceSqlOrDecodeError(
"AuthSessionRepository.revoke:query",
"AuthSessionRepository.revoke:decodeRows",
{ sessionId: input.sessionId },
),
),
Effect.map((rows) => rows.length > 0),
);
const revokeAllExcept: AuthSessionRepository["Service"]["revokeAllExcept"] = (input) =>
revokeOtherSessionRows(input).pipe(
Effect.mapError(
toPersistenceSqlOrDecodeError(
"AuthSessionRepository.revokeAllExcept:query",
"AuthSessionRepository.revokeAllExcept:decodeRows",
{ currentSessionId: input.currentSessionId },
),
),
Effect.map((rows) => rows.map((row) => row.sessionId)),
);
const setLastConnectedAt: AuthSessionRepository["Service"]["setLastConnectedAt"] = (input) =>
setLastConnectedAtRow(input).pipe(
Effect.mapError(
toPersistenceSqlOrDecodeError(
"AuthSessionRepository.setLastConnectedAt:query",
"AuthSessionRepository.setLastConnectedAt:encodeRequest",
{ sessionId: input.sessionId },
),
),
);
return {
create,
getById,
listActive,
revoke,
revokeAllExcept,
setLastConnectedAt,
} satisfies AuthSessionRepository["Service"];
});
export const layer = Layer.effect(AuthSessionRepository, make);