@nyc-transit-kit/soda3 0.1.4 → 0.3.0

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/src/Soql.ts ADDED
@@ -0,0 +1,200 @@
1
+ /**
2
+ * SoQL as `effect/sql` fragments.
3
+ *
4
+ * Text written in a `soql` template is trusted SoQL. Every interpolated value
5
+ * becomes a quoted literal and every `identifier` a backticked column, so input
6
+ * can never change the shape of a query. SODA3 has no bind parameters (`:name`
7
+ * names a system column), so `compile` writes the values into the text.
8
+ */
9
+ import * as Effect from "effect/Effect"
10
+ import * as Schema from "effect/Schema"
11
+ import * as Statement from "effect/sql/Statement"
12
+ import type { IsoDate } from "./Socrata"
13
+
14
+ export type Fragment = Statement.Fragment
15
+
16
+ /** A value SoQL can write as a literal. Timestamps are strings such as `2026-10-01T00:00:00`. */
17
+ export type Value = string | number | boolean
18
+
19
+ /** A fragment that cannot be written as SoQL, or clauses that make no sense. */
20
+ export class SoqlError extends Schema.TaggedError<SoqlError>()("SoqlError", {
21
+ message: Schema.String
22
+ }) {}
23
+
24
+ /** A column or alias, written as `` `name` `` so that reserved words such as `from` work. */
25
+ export const identifier = (name: string): Statement.Identifier => Statement.identifier(name)
26
+
27
+ /**
28
+ * `` soql`speed > ${limit}` ``. The text is SoQL; each interpolation is a value,
29
+ * an `identifier`, or another fragment spliced in place.
30
+ */
31
+ export const soql = (
32
+ strings: TemplateStringsArray,
33
+ ...args: ReadonlyArray<Value | Fragment | Statement.Segment>
34
+ ): Fragment => {
35
+ const segments: Array<Statement.Segment> = []
36
+ strings.forEach((text, index) => {
37
+ if (text !== "") {
38
+ segments.push(Statement.literal(text))
39
+ }
40
+ if (index < args.length) {
41
+ const arg = args[index]
42
+ if (Statement.isFragment(arg)) {
43
+ segments.push(...arg.segments)
44
+ } else if (typeof arg === "object") {
45
+ segments.push(arg)
46
+ } else if (arg !== undefined) {
47
+ segments.push(Statement.parameter(arg))
48
+ }
49
+ }
50
+ })
51
+ return Statement.fragment(segments)
52
+ }
53
+
54
+ const quote = (text: string) => `'${text.replaceAll("'", "''")}'`
55
+
56
+ const write = (value: unknown) => (typeof value === "string" ? quote(value) : String(value))
57
+
58
+ const compiler = Statement.makeCompiler({
59
+ // `Statement` knows no SoQL dialect. The name only matters to dialect switches, which SoQL never uses.
60
+ dialect: "pg",
61
+ placeholder: (_index, value) => write(value),
62
+ onIdentifier: (name) => `\`${name}\``,
63
+ onRecordUpdate: () => ["", []],
64
+ onCustom: () => ["", []]
65
+ })
66
+
67
+ const invalidValue = (value: unknown) =>
68
+ typeof value === "string" ||
69
+ typeof value === "boolean" ||
70
+ (typeof value === "number" && Number.isFinite(value))
71
+ ? undefined
72
+ : `SoQL has no literal for ${String(value)}`
73
+
74
+ const invalidSegment = (segment: Statement.Segment): string | undefined => {
75
+ switch (segment._tag) {
76
+ case "Literal":
77
+ return undefined
78
+ case "Identifier":
79
+ // SoQL has no escape for a backtick inside a quoted name.
80
+ return segment.value === "" || segment.value.includes("`")
81
+ ? `SoQL cannot name the column ${JSON.stringify(segment.value)}`
82
+ : undefined
83
+ case "Parameter":
84
+ return invalidValue(segment.value)
85
+ case "ArrayHelper":
86
+ return segment.value.length === 0
87
+ ? "SoQL IN needs at least one value"
88
+ : segment.value.map(invalidValue).find((problem) => problem !== undefined)
89
+ default:
90
+ return `SoQL has no ${segment._tag}`
91
+ }
92
+ }
93
+
94
+ /** The SoQL text of a fragment. Fails on a column SoQL cannot name or a value it cannot write. */
95
+ export const compile = (fragment: Fragment): Effect.Effect<string, SoqlError> => {
96
+ const problem = fragment.segments.map(invalidSegment).find((message) => message !== undefined)
97
+ return problem === undefined
98
+ ? Effect.succeed(compiler.compile(fragment, false)[0])
99
+ : Effect.fail(new SoqlError({ message: problem }))
100
+ }
101
+
102
+ export const eq = (column: string, value: Value): Fragment => soql`${identifier(column)} = ${value}`
103
+
104
+ export const isIn = (column: string, values: readonly [Value, ...Array<Value>]): Fragment =>
105
+ soql`${identifier(column)} IN ${Statement.arrayHelper(values)}`
106
+
107
+ export const isNotNull = (column: string): Fragment => soql`${identifier(column)} IS NOT NULL`
108
+
109
+ const joinParenthesized =
110
+ (separator: string) =>
111
+ (clauses: readonly [Fragment, ...Array<Fragment>]): Fragment =>
112
+ Statement.join(separator, false)(clauses.map((clause) => soql`(${clause})`))
113
+
114
+ /** `(a) AND (b)`. Each clause keeps its own parentheses, so an `OR` inside one stays inside it. */
115
+ export const and = joinParenthesized(" AND ")
116
+
117
+ /** `(a) OR (b)`. */
118
+ export const or = joinParenthesized(" OR ")
119
+
120
+ export type SortDirection = "ASC" | "DESC"
121
+
122
+ export interface OrderBy {
123
+ readonly column: string
124
+ readonly direction?: SortDirection | undefined
125
+ }
126
+
127
+ /** The clauses of a query over whole rows. */
128
+ export interface Clauses {
129
+ readonly where?: Fragment | undefined
130
+ readonly orderBy?: OrderBy | undefined
131
+ readonly limit?: number | undefined
132
+ }
133
+
134
+ /** `SELECT *` with each clause that is given, in SoQL's order. */
135
+ export const selectAll = ({ where, orderBy, limit }: Clauses): Fragment =>
136
+ Statement.fragment([
137
+ Statement.literal("SELECT *"),
138
+ ...(where === undefined ? [] : [Statement.literal(" WHERE "), ...where.segments]),
139
+ ...(orderBy === undefined
140
+ ? []
141
+ : [
142
+ Statement.literal(" ORDER BY "),
143
+ identifier(orderBy.column),
144
+ Statement.literal(` ${orderBy.direction ?? "ASC"}`)
145
+ ]),
146
+ ...(limit === undefined ? [] : [Statement.literal(" LIMIT "), Statement.parameter(limit)])
147
+ ])
148
+
149
+ /** A half-open window of Socrata floating timestamps, `start <= t < end`. */
150
+ export interface TimestampWindow {
151
+ readonly start: string
152
+ readonly end: string
153
+ }
154
+
155
+ export interface YearMonth {
156
+ readonly year: number
157
+ readonly month: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12
158
+ }
159
+
160
+ const monthStart = (year: number, month: number) =>
161
+ `${year.toString().padStart(4, "0")}-${month.toString().padStart(2, "0")}-01T00:00:00`
162
+
163
+ /** Midnight at the start of a calendar date, as a floating timestamp. */
164
+ export const timestamp = (date: IsoDate) => `${date}T00:00:00`
165
+
166
+ /** The calendar month as a window. */
167
+ export const monthWindow = ({ year, month }: YearMonth): TimestampWindow => ({
168
+ start: monthStart(year, month),
169
+ end: month === 12 ? monthStart(year + 1, 1) : monthStart(year, month + 1)
170
+ })
171
+
172
+ /** The days from `start` up to, not including, `end`. Fails when the window is empty. */
173
+ export const dateWindow = (start: IsoDate, end: IsoDate) =>
174
+ start < end
175
+ ? Effect.succeed<TimestampWindow>({ start: timestamp(start), end: timestamp(end) })
176
+ : Effect.fail(new SoqlError({ message: `Date window ${start}..${end} is empty` }))
177
+
178
+ /** `column` inside the window. */
179
+ export const timestampRange = (column: string, window: TimestampWindow): Fragment =>
180
+ soql`${identifier(column)} >= ${window.start} AND ${identifier(column)} < ${window.end}`
181
+
182
+ /** `column` inside the calendar month. */
183
+ export const monthRange = (column: string, month: YearMonth): Fragment =>
184
+ timestampRange(column, monthWindow(month))
185
+
186
+ /** Rows whose separate year and month columns fall in `[start, end]`, both months included. */
187
+ export const yearMonthRange = (
188
+ columns: { readonly year: string; readonly month: string },
189
+ start: YearMonth,
190
+ end: YearMonth
191
+ ) => {
192
+ if (start.year * 12 + start.month > end.year * 12 + end.month) {
193
+ return Effect.fail(new SoqlError({ message: "Year/month range starts after it ends" }))
194
+ }
195
+ const year = identifier(columns.year)
196
+ const month = identifier(columns.month)
197
+ return Effect.succeed(
198
+ soql`(${year} > ${start.year} OR (${year} = ${start.year} AND ${month} >= ${start.month})) AND (${year} < ${end.year} OR (${year} = ${end.year} AND ${month} <= ${end.month}))`
199
+ )
200
+ }
package/src/index.ts CHANGED
@@ -1,61 +1,5 @@
1
- export const packageName = "@nyc-transit-kit/soda3"
2
- export { buildCatalogSearchUrl, catalogSearch } from "./catalog"
3
- export type { FetchImplementation, Soda3ClientConfigShape, Soda3LayerOptions } from "./client"
4
- export {
5
- Soda3ClientConfig,
6
- Soda3HttpLive,
7
- Soda3Live,
8
- soda3ConfigLayer,
9
- soda3FetchLayer,
10
- soda3Layer
11
- } from "./client"
12
- export {
13
- buildExportUrl,
14
- buildQueryUrl,
15
- decodeSoda3CatalogSearchRequest,
16
- decodeSoda3ExportRequest,
17
- decodeSoda3QueryRequest,
18
- defaultSocrataProtocol,
19
- discoveryApiHost,
20
- socrataApiVersion
21
- } from "./endpoints"
22
- export type { Soda3ClientError } from "./errors"
23
- export {
24
- InvalidInputError,
25
- isRetryableProviderError,
26
- isSoda3ClientError,
27
- ProviderContractError,
28
- ProviderHttpError,
29
- RetryExhaustedError,
30
- TimeoutError
31
- } from "./errors"
32
- export { exportResponse } from "./export"
33
- export type { Soda3DatasetMetadataRequestInput } from "./metadata"
34
- export { fetchDatasetMetadata } from "./metadata"
35
- export type { Soda3CountRowsInput, Soda3QueryAllRowsInput } from "./query"
36
- export { countRows, queryAllRows, queryRows } from "./query"
37
- export type {
38
- SocrataTimestampWindow,
39
- SocrataYearMonth,
40
- SoqlFragment,
41
- SoqlParameters,
42
- SoqlSortDirection
43
- } from "./soql"
44
- export {
45
- socrataDateWindow,
46
- socrataMonthWindow,
47
- socrataTimestamp,
48
- soqlAnd,
49
- soqlEq,
50
- soqlIdentifier,
51
- soqlIn,
52
- soqlIsNotNull,
53
- soqlLimit,
54
- soqlMonthWindow,
55
- soqlOrderBy,
56
- soqlParameter,
57
- soqlParameterName,
58
- soqlSelectAll,
59
- soqlTimestampRange,
60
- soqlYearMonthRange
61
- } from "./soql"
1
+ export * as Dataset from "./Dataset"
2
+ export * as Discovery from "./Discovery"
3
+ export * as Socrata from "./Socrata"
4
+ export * as Soda3Client from "./Soda3Client"
5
+ export * as Soql from "./Soql"
package/src/catalog.ts DELETED
@@ -1,28 +0,0 @@
1
- import type { Soda3CatalogSearchRequestInput } from "@nyc-transit-kit/contracts/soda3"
2
- import * as Effect from "effect/Effect"
3
- import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"
4
- import { buildCatalogSearchUrl } from "./endpoints"
5
- import { decodeCatalogSearchResponse, decodeJson } from "./internal/responses"
6
- import { Soda3ClientConfig } from "./internal/services"
7
- import { addHeaders, executeRequest, requestWithPolicy } from "./internal/transport"
8
-
9
- export { buildCatalogSearchUrl } from "./endpoints"
10
- export type { Soda3CatalogSearchRequestInput }
11
-
12
- export const catalogSearch = Effect.fn("Soda3.catalogSearch")(function* (
13
- input: Soda3CatalogSearchRequestInput
14
- ) {
15
- const config = yield* Soda3ClientConfig
16
- const url = yield* buildCatalogSearchUrl(input)
17
- const request = HttpClientRequest.get(url, {
18
- headers: addHeaders(config)
19
- })
20
-
21
- return yield* requestWithPolicy(
22
- "catalog",
23
- executeRequest("catalog", request).pipe(
24
- Effect.flatMap((response) => decodeJson("catalog", response)),
25
- Effect.flatMap(decodeCatalogSearchResponse)
26
- )
27
- )
28
- })
package/src/client.ts DELETED
@@ -1,17 +0,0 @@
1
- export { catalogSearch } from "./catalog"
2
- export { exportResponse } from "./export"
3
- export { Soda3HttpLive } from "./internal/http"
4
- export type {
5
- FetchImplementation,
6
- Soda3ClientConfigShape,
7
- Soda3LayerOptions
8
- } from "./internal/services"
9
- export {
10
- Soda3ClientConfig,
11
- Soda3Live,
12
- soda3ConfigLayer,
13
- soda3FetchLayer,
14
- soda3Layer
15
- } from "./internal/services"
16
- export { fetchDatasetMetadata } from "./metadata"
17
- export { countRows, queryAllRows, queryRows } from "./query"
package/src/endpoints.ts DELETED
@@ -1,110 +0,0 @@
1
- import {
2
- Soda3CatalogSearchRequest,
3
- type Soda3CatalogSearchRequestInput,
4
- Soda3ExportRequest,
5
- type Soda3ExportRequestInput,
6
- Soda3QueryRequest,
7
- type Soda3QueryRequestInput
8
- } from "@nyc-transit-kit/contracts/soda3"
9
- import * as Effect from "effect/Effect"
10
- import * as Schema from "effect/Schema"
11
- import { InvalidInputError } from "./errors"
12
-
13
- export const socrataApiVersion = "v3"
14
- export const defaultSocrataProtocol = "https"
15
- export const discoveryApiHost = "api.us.socrata.com"
16
-
17
- const decodeQueryRequest = (input: unknown) =>
18
- Schema.decodeUnknownEffect(Soda3QueryRequest)(input).pipe(
19
- Effect.catchTag("SchemaError", (error) =>
20
- Effect.fail(
21
- InvalidInputError.make({
22
- operation: "query",
23
- message: error.message
24
- })
25
- )
26
- )
27
- )
28
-
29
- const decodeExportRequest = (input: unknown) =>
30
- Schema.decodeUnknownEffect(Soda3ExportRequest)(input).pipe(
31
- Effect.catchTag("SchemaError", (error) =>
32
- Effect.fail(
33
- InvalidInputError.make({
34
- operation: "export",
35
- message: error.message
36
- })
37
- )
38
- )
39
- )
40
-
41
- const decodeCatalogSearchRequest = (input: unknown) =>
42
- Schema.decodeUnknownEffect(Soda3CatalogSearchRequest)(input).pipe(
43
- Effect.catchTag("SchemaError", (error) =>
44
- Effect.fail(
45
- InvalidInputError.make({
46
- operation: "catalog",
47
- message: error.message
48
- })
49
- )
50
- )
51
- )
52
-
53
- const viewUrl = (domain: string, datasetId: string, suffix: string) => {
54
- const url = new URL(`${defaultSocrataProtocol}://${domain}`)
55
- url.pathname = `/api/${socrataApiVersion}/views/${datasetId}/${suffix}`
56
- return url
57
- }
58
-
59
- export const buildQueryUrl = Effect.fn("Soda3.buildQueryUrl")(function* (
60
- input: Soda3QueryRequestInput
61
- ) {
62
- const request = yield* decodeQueryRequest(input)
63
- return viewUrl(request.domain, request.datasetId, "query.json")
64
- })
65
-
66
- export const buildExportUrl = Effect.fn("Soda3.buildExportUrl")(function* (
67
- input: Soda3ExportRequestInput
68
- ) {
69
- const request = yield* decodeExportRequest(input)
70
- return viewUrl(request.domain, request.datasetId, `export.${request.format}`)
71
- })
72
-
73
- export const buildCatalogSearchUrl = Effect.fn("Soda3.buildCatalogSearchUrl")(function* (
74
- input: Soda3CatalogSearchRequestInput
75
- ) {
76
- const request = yield* decodeCatalogSearchRequest(input)
77
- const url = new URL(`${defaultSocrataProtocol}://${discoveryApiHost}`)
78
- url.pathname = "/api/catalog/v1"
79
- url.searchParams.set("domains", request.domain)
80
- url.searchParams.set("search_context", request.domain)
81
- url.searchParams.set("only", "dataset")
82
-
83
- if (request.query !== undefined) {
84
- url.searchParams.set("search", request.query)
85
- }
86
- for (const id of request.ids ?? []) {
87
- url.searchParams.append("ids", id)
88
- }
89
- for (const category of request.categories ?? []) {
90
- url.searchParams.append("categories", category)
91
- }
92
- for (const tag of request.tags ?? []) {
93
- url.searchParams.append("tags", tag)
94
- }
95
- if (request.order !== undefined) {
96
- url.searchParams.set("order", request.order)
97
- }
98
- if (request.limit !== undefined) {
99
- url.searchParams.set("limit", request.limit.toString())
100
- }
101
- if (request.offset !== undefined) {
102
- url.searchParams.set("offset", request.offset.toString())
103
- }
104
-
105
- return url
106
- })
107
-
108
- export const decodeSoda3QueryRequest = decodeQueryRequest
109
- export const decodeSoda3ExportRequest = decodeExportRequest
110
- export const decodeSoda3CatalogSearchRequest = decodeCatalogSearchRequest
package/src/errors.ts DELETED
@@ -1,58 +0,0 @@
1
- import * as Schema from "effect/Schema"
2
-
3
- export class InvalidInputError extends Schema.TaggedErrorClass<InvalidInputError>()(
4
- "InvalidInputError",
5
- {
6
- operation: Schema.String,
7
- message: Schema.String
8
- }
9
- ) {}
10
-
11
- export class ProviderHttpError extends Schema.TaggedErrorClass<ProviderHttpError>()(
12
- "ProviderHttpError",
13
- {
14
- operation: Schema.String,
15
- status: Schema.Number,
16
- statusText: Schema.String,
17
- retryable: Schema.Boolean
18
- }
19
- ) {}
20
-
21
- export class ProviderContractError extends Schema.TaggedErrorClass<ProviderContractError>()(
22
- "ProviderContractError",
23
- {
24
- operation: Schema.String,
25
- message: Schema.String
26
- }
27
- ) {}
28
-
29
- export class TimeoutError extends Schema.TaggedErrorClass<TimeoutError>()("TimeoutError", {
30
- operation: Schema.String,
31
- timeoutMs: Schema.Number
32
- }) {}
33
-
34
- export class RetryExhaustedError extends Schema.TaggedErrorClass<RetryExhaustedError>()(
35
- "RetryExhaustedError",
36
- {
37
- operation: Schema.String,
38
- attempts: Schema.Number,
39
- message: Schema.String
40
- }
41
- ) {}
42
-
43
- export type Soda3ClientError =
44
- | InvalidInputError
45
- | ProviderHttpError
46
- | ProviderContractError
47
- | TimeoutError
48
- | RetryExhaustedError
49
-
50
- export const isSoda3ClientError = (value: unknown): value is Soda3ClientError =>
51
- value instanceof InvalidInputError ||
52
- value instanceof ProviderHttpError ||
53
- value instanceof ProviderContractError ||
54
- value instanceof TimeoutError ||
55
- value instanceof RetryExhaustedError
56
-
57
- export const isRetryableProviderError = (error: Soda3ClientError): error is ProviderHttpError =>
58
- error._tag === "ProviderHttpError" && error.retryable
package/src/export.ts DELETED
@@ -1,41 +0,0 @@
1
- import type { Soda3ExportRequestInput } from "@nyc-transit-kit/contracts/soda3"
2
- import * as Effect from "effect/Effect"
3
- import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"
4
- import { buildExportUrl, decodeSoda3ExportRequest } from "./endpoints"
5
- import { InvalidInputError } from "./errors"
6
- import { rangeHeader, toExportBody } from "./internal/bodies"
7
- import { toWebResponse } from "./internal/http"
8
- import { Soda3ClientConfig } from "./internal/services"
9
- import { addHeaders, executeRequest, requestWithPolicy } from "./internal/transport"
10
-
11
- export type { Soda3ExportRequestInput }
12
-
13
- export const exportResponse = Effect.fn("Soda3.exportResponse")(function* (
14
- input: Soda3ExportRequestInput
15
- ) {
16
- const config = yield* Soda3ClientConfig
17
- const exportRequest = yield* decodeSoda3ExportRequest(input)
18
- const url = yield* buildExportUrl(exportRequest)
19
- const headers = addHeaders(config)
20
- const range = rangeHeader(exportRequest)
21
-
22
- const request = yield* HttpClientRequest.post(url, {
23
- headers: {
24
- ...headers,
25
- range
26
- }
27
- }).pipe(
28
- HttpClientRequest.bodyJson(toExportBody(exportRequest)),
29
- Effect.mapError(() =>
30
- InvalidInputError.make({
31
- operation: "export",
32
- message: "SODA3 export body was not JSON serializable"
33
- })
34
- )
35
- )
36
-
37
- return yield* requestWithPolicy(
38
- "export",
39
- executeRequest("export", request).pipe(Effect.flatMap(toWebResponse))
40
- )
41
- })
@@ -1,23 +0,0 @@
1
- import type { Soda3ExportRequest, Soda3QueryRequest } from "@nyc-transit-kit/contracts/soda3"
2
-
3
- export const toQueryBody = (request: Soda3QueryRequest) => ({
4
- query: request.query,
5
- page: request.page,
6
- parameters: request.parameters,
7
- timeout: request.timeout,
8
- includeSystem: request.includeSystem,
9
- includeSynthetic: request.includeSynthetic,
10
- orderingSpecifier: request.orderingSpecifier
11
- })
12
-
13
- export const toExportBody = (request: Soda3ExportRequest) => ({
14
- query: request.query,
15
- parameters: request.parameters,
16
- timeout: request.timeout,
17
- serializationOptions: request.serializationOptions
18
- })
19
-
20
- export const rangeHeader = (request: Soda3ExportRequest) =>
21
- request.range === undefined
22
- ? undefined
23
- : `bytes=${request.range.start}-${request.range.end ?? ""}`
@@ -1,42 +0,0 @@
1
- import * as Effect from "effect/Effect"
2
- import * as Stream from "effect/Stream"
3
- import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"
4
- import type * as HttpClientError from "effect/unstable/http/HttpClientError"
5
- import type * as HttpClientResponse from "effect/unstable/http/HttpClientResponse"
6
- import { ProviderHttpError } from "../errors"
7
-
8
- export const Soda3HttpLive = FetchHttpClient.layer
9
-
10
- export const retryableStatus = (status: number) => status === 429 || status >= 500
11
-
12
- const httpStatusText = (status: number) => `HTTP ${status}`
13
-
14
- export const toWebResponse = (response: HttpClientResponse.HttpClientResponse) =>
15
- Effect.map(
16
- Stream.toReadableStreamEffect(response.stream),
17
- (body) =>
18
- new Response(body, {
19
- status: response.status,
20
- headers: response.headers
21
- })
22
- )
23
-
24
- export const mapHttpClientError = (operation: string, error: HttpClientError.HttpClientError) => {
25
- const response = error.response
26
-
27
- if (response !== undefined) {
28
- return ProviderHttpError.make({
29
- operation,
30
- status: response.status,
31
- statusText: httpStatusText(response.status),
32
- retryable: retryableStatus(response.status)
33
- })
34
- }
35
-
36
- return ProviderHttpError.make({
37
- operation,
38
- status: 0,
39
- statusText: error.message,
40
- retryable: true
41
- })
42
- }
@@ -1,56 +0,0 @@
1
- import {
2
- Soda3CatalogSearchResponse,
3
- Soda3QueryResponse,
4
- Soda3Row
5
- } from "@nyc-transit-kit/contracts/soda3"
6
- import * as Effect from "effect/Effect"
7
- import * as Schema from "effect/Schema"
8
- import type * as HttpClientResponse from "effect/unstable/http/HttpClientResponse"
9
- import { ProviderContractError } from "../errors"
10
-
11
- const providerRows = Schema.Array(Soda3Row)
12
-
13
- export const decodeJson = (operation: string, response: HttpClientResponse.HttpClientResponse) =>
14
- response.json.pipe(
15
- Effect.catchTag("HttpClientError", () =>
16
- Effect.fail(
17
- ProviderContractError.make({
18
- operation,
19
- message: "Provider response was not valid JSON"
20
- })
21
- )
22
- )
23
- )
24
-
25
- export const decodeQueryResponse = (input: unknown) =>
26
- Schema.decodeUnknownEffect(Soda3QueryResponse)(input).pipe(
27
- Effect.catchTag("SchemaError", (error) =>
28
- Schema.decodeUnknownEffect(providerRows)(input).pipe(
29
- Effect.map((rows) =>
30
- Soda3QueryResponse.make({
31
- rows
32
- })
33
- ),
34
- Effect.catchTag("SchemaError", () =>
35
- Effect.fail(
36
- ProviderContractError.make({
37
- operation: "query",
38
- message: error.message
39
- })
40
- )
41
- )
42
- )
43
- )
44
- )
45
-
46
- export const decodeCatalogSearchResponse = (input: unknown) =>
47
- Schema.decodeUnknownEffect(Soda3CatalogSearchResponse)(input).pipe(
48
- Effect.catchTag("SchemaError", (error) =>
49
- Effect.fail(
50
- ProviderContractError.make({
51
- operation: "catalog",
52
- message: error.message
53
- })
54
- )
55
- )
56
- )