(
options: { readonly directory?: string } = {},
)
| 70 | findings.length === 0 ? Effect.void : Effect.fail(new UnsafeCassetteError({ cassetteName: name, findings })) |
| 71 | |
| 72 | export const fileSystem = ( |
| 73 | options: { readonly directory?: string } = {}, |
| 74 | ): Layer.Layer<Service, never, FileSystem.FileSystem> => |
| 75 | Layer.effect( |
| 76 | Service, |
| 77 | Effect.gen(function* () { |
| 78 | const fs = yield* FileSystem.FileSystem |
| 79 | const directory = options.directory ?? DEFAULT_RECORDINGS_DIR |
| 80 | const recorded = new Map<string, { interactions: Interaction[]; findings: SecretFinding[] }>() |
| 81 | const appendLock = yield* Semaphore.make(1) |
| 82 | |
| 83 | const pathFor = (name: string) => cassettePath(directory, name) |
| 84 | |
| 85 | const walk = (current: string): Effect.Effect<ReadonlyArray<string>> => |
| 86 | Effect.gen(function* () { |
| 87 | const entries = yield* fs.readDirectory(current).pipe(Effect.catch(() => Effect.succeed([] as string[]))) |
| 88 | const nested = yield* Effect.forEach(entries, (entry) => { |
| 89 | const full = path.join(current, entry) |
| 90 | return fs.stat(full).pipe( |
| 91 | Effect.flatMap((stat) => (stat.type === "Directory" ? walk(full) : Effect.succeed([full]))), |
| 92 | Effect.catch(() => Effect.succeed([] as string[])), |
| 93 | ) |
| 94 | }) |
| 95 | return nested.flat() |
| 96 | }) |
| 97 | |
| 98 | return Service.of({ |
| 99 | read: (name) => |
| 100 | fs.readFileString(pathFor(name)).pipe( |
| 101 | Effect.map((raw) => parseCassette(raw).interactions), |
| 102 | Effect.catch(() => Effect.fail(new CassetteNotFoundError({ cassetteName: name }))), |
| 103 | ), |
| 104 | append: (name, interaction, metadata) => |
| 105 | appendLock.withPermit( |
| 106 | Effect.gen(function* () { |
| 107 | const entry = recorded.get(name) ?? { interactions: [], findings: [] } |
| 108 | const interactions = [...entry.interactions, interaction] |
| 109 | const interactionFindings = [...entry.findings, ...secretFindings(interaction)] |
| 110 | const cassette = buildCassette(name, interactions, metadata) |
| 111 | const findings = [...interactionFindings, ...secretFindings(cassette.metadata ?? {})] |
| 112 | yield* failIfUnsafe(name, findings) |
| 113 | const target = pathFor(name) |
| 114 | yield* fs.makeDirectory(path.dirname(target), { recursive: true }).pipe(Effect.orDie) |
| 115 | const temporary = `${target}.${crypto.randomUUID()}.tmp` |
| 116 | yield* fs.writeFileString(temporary, formatCassette(cassette)).pipe( |
| 117 | Effect.flatMap(() => fs.rename(temporary, target)), |
| 118 | Effect.ensuring(fs.remove(temporary, { force: true }).pipe(Effect.catch(() => Effect.void))), |
| 119 | Effect.orDie, |
| 120 | ) |
| 121 | recorded.set(name, { interactions, findings: interactionFindings }) |
| 122 | }), |
| 123 | ), |
| 124 | exists: (name) => |
| 125 | fs.access(pathFor(name)).pipe( |
| 126 | Effect.as(true), |
| 127 | Effect.catch(() => Effect.succeed(false)), |
| 128 | ), |
| 129 | list: () => |
nothing calls this directly
no test coverage detected