import { Effect } from 'effect'; import { EntitySchema, SingleEntitySchema, type DecodedEntity, type DecodedSingleEntity, type EncodedEntity, type EncodedSingleEntity, } from '../core/index.js'; import type { EncodedData, StdTableService, } from '../db/std-table/contract/index.js'; import type { KeyedEntityDefinition, SingleEntityDefinition, } from '../db/std-table/definition/index.js'; import { makeKeyedEntity, makeSingleEntity, type QueryPage, } from '../db/std-table/entity/index.js'; import type { DatabaseError } from '../db/std-table/error/index.js'; import type { StdTable } from '../db/std-table/table/index.js'; import { StudioEntityCodecFailed, StudioInvalidInput, StudioReadFailed, StudioUnknownAccessPattern, StudioUnknownEntity, StudioWrongEntityKind, type GetEntityPayload, type QueryEntitiesPayload, } from './protocol.js'; import { buildEntityQueryInput, rejectSingletonKey, validateEntityKey, } from './query-input.js'; type StudioEncodedEntity = EncodedEntity; type StudioEncodedSingleEntity = EncodedSingleEntity; interface DynamicKeyedEntity { readonly get: ( key: object, ) => Effect.Effect< DecodedEntity | null, DatabaseError, StdTableService >; readonly query: ( pattern: string, input: object, options?: { readonly limit?: number; readonly after?: DecodedEntity; }, ) => Effect.Effect< QueryPage>, DatabaseError, StdTableService >; } interface DynamicSingleEntity { readonly get: () => Effect.Effect< DecodedSingleEntity, DatabaseError, StdTableService >; } const keyedSurface = (definition: KeyedEntityDefinition) => makeKeyedEntity( definition as KeyedEntityDefinition< string, typeof definition.schema, readonly string[], readonly [string] >, ) as unknown as DynamicKeyedEntity; const singleSurface = ( definition: SingleEntityDefinition, ) => makeSingleEntity(definition) as unknown as DynamicSingleEntity; const codecFailure = ( entity: string, direction: StudioEntityCodecFailed['direction'], ) => new StudioEntityCodecFailed({ entity, direction }); const readFailure = (operation: StudioReadFailed['operation'], entity: string) => (error: DatabaseError) => { if (error.reason._tag === 'DecodeFailed') return codecFailure(entity, 'decode-read'); if (error.reason._tag === 'InvalidQuery') return new StudioInvalidInput({ issues: [{ path: [], message: error.reason.message }], }); return new StudioReadFailed({ entity, operation }); }; const preserveReadFailure = (operation: StudioReadFailed['operation'], entity: string) => (effect: Effect.Effect) => effect.pipe( Effect.tapError((error) => Effect.logError(error)), Effect.mapError(readFailure(operation, entity)), ); const encodeKeyed = (definition: KeyedEntityDefinition) => (entity: DecodedEntity) => EntitySchema(definition.schema) .encode(entity) .pipe( Effect.tapError((error) => Effect.logError(error)), Effect.mapError(() => codecFailure(definition.name, 'encode-result')), Effect.map((encoded) => encoded as StudioEncodedEntity), ); const encodeSingle = (definition: SingleEntityDefinition) => (entity: DecodedSingleEntity) => SingleEntitySchema(definition.schema) .encode(entity) .pipe( Effect.tapError((error) => Effect.logError(error)), Effect.mapError(() => codecFailure(definition.name, 'encode-result')), Effect.map((encoded) => encoded as StudioEncodedSingleEntity), ); const definitionNamed = ( table: StdTable, entity: string, ) => table.registeredEntities.find((definition) => definition.name === entity); export const makeEntityReader = ( table: StdTable, ) => { const get = (payload: GetEntityPayload) => Effect.gen(function* () { const definition = definitionNamed(table, payload.entity); if (definition === undefined) return yield* Effect.fail( new StudioUnknownEntity({ entity: payload.entity }), ); if (definition.kind === 'single') { yield* rejectSingletonKey(payload.key); const entity = yield* singleSurface(definition) .get() .pipe(preserveReadFailure('get', definition.name)); return yield* encodeSingle(definition)(entity); } const key = yield* validateEntityKey(payload.key, [ ...definition.primary.pk, definition.schema.idField, ]); const entity = yield* keyedSurface(definition) .get(key) .pipe(preserveReadFailure('get', definition.name)); return entity === null ? null : yield* encodeKeyed(definition)(entity); }); const query = (payload: QueryEntitiesPayload) => Effect.gen(function* () { const definition = definitionNamed(table, payload.entity); if (definition === undefined) return yield* Effect.fail( new StudioUnknownEntity({ entity: payload.entity }), ); if (definition.kind === 'single') return yield* Effect.fail( new StudioWrongEntityKind({ entity: definition.name, expected: 'keyed', actual: 'single', }), ); const pattern = Object.hasOwn( definition.accessPatterns, payload.accessPattern, ) ? definition.accessPatterns[payload.accessPattern] : undefined; if (pattern === undefined) return yield* Effect.fail( new StudioUnknownAccessPattern({ entity: definition.name, accessPattern: payload.accessPattern, }), ); const input = yield* buildEntityQueryInput(payload, pattern); const after = payload.after === undefined ? undefined : yield* EntitySchema(definition.schema) .decode(payload.after) .pipe( Effect.mapError(() => codecFailure(definition.name, 'decode-after'), ), ); const page = yield* keyedSurface(definition) .query(payload.accessPattern, input, { limit: payload.limit ?? 100, ...(after === undefined ? {} : { after }), }) .pipe(preserveReadFailure('query', definition.name)); const items = yield* Effect.forEach(page.items, encodeKeyed(definition)); return { items, hasMore: page.hasMore }; }); return { get, query } as const; };