@nyc-transit-kit/cli 0.1.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/LICENSE +21 -0
- package/README.md +5 -0
- package/bin/ntk +3 -0
- package/bin/nyc-transit +3 -0
- package/package.json +58 -0
- package/scripts/build-binary.ts +113 -0
- package/scripts/build.ts +1 -0
- package/src/build-info.ts +2 -0
- package/src/commands/catalog.ts +59 -0
- package/src/commands/mta.ts +610 -0
- package/src/commands/nyc-dot.ts +212 -0
- package/src/commands/nyc-open-data.ts +243 -0
- package/src/commands/root.ts +38 -0
- package/src/commands/shared.ts +144 -0
- package/src/commands/socrata.ts +206 -0
- package/src/commands/soda3-shared.ts +103 -0
- package/src/commands/types.ts +33 -0
- package/src/config.ts +21 -0
- package/src/files.ts +27 -0
- package/src/index.ts +2 -0
- package/src/main.ts +205 -0
- package/src/output.ts +39 -0
- package/src/runtime.ts +14 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import type { Soda3ExportFormat } from "@nyc-transit-kit/contracts/soda3"
|
|
2
|
+
import { exportNycDotDataset, queryNycDotDataset } from "@nyc-transit-kit/nyc-dot/client"
|
|
3
|
+
import {
|
|
4
|
+
findNycDotDataset,
|
|
5
|
+
nycDotDatasets,
|
|
6
|
+
nycDotOpenDataDomain
|
|
7
|
+
} from "@nyc-transit-kit/nyc-dot/datasets"
|
|
8
|
+
import * as Effect from "effect/Effect"
|
|
9
|
+
import * as Command from "effect/unstable/cli/Command"
|
|
10
|
+
import * as Flag from "effect/unstable/cli/Flag"
|
|
11
|
+
import { failCommand, runEffect, soqlFromSelect, writeSuccess } from "./shared"
|
|
12
|
+
import { writeResponseToFile, writeSoda3ExportDryRun, writeSoda3QueryDryRun } from "./soda3-shared"
|
|
13
|
+
import { CliCommandContext } from "./types"
|
|
14
|
+
|
|
15
|
+
export const nycDotCommandPaths: ReadonlyArray<ReadonlyArray<string>> = [
|
|
16
|
+
["nyc-dot", "dataset", "list"],
|
|
17
|
+
["nyc-dot", "dataset", "info"],
|
|
18
|
+
["nyc-dot", "dataset", "query"],
|
|
19
|
+
["nyc-dot", "dataset", "export"]
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
export const handleNycDotList = () =>
|
|
23
|
+
Effect.gen(function* () {
|
|
24
|
+
const context = yield* CliCommandContext
|
|
25
|
+
return yield* writeSuccess(context, "nyc-dot", {
|
|
26
|
+
datasets: nycDotDatasets
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
export const handleNycDotInfo = (config: { readonly name: string }) =>
|
|
31
|
+
Effect.gen(function* () {
|
|
32
|
+
const context = yield* CliCommandContext
|
|
33
|
+
const descriptor = findNycDotDataset(config.name)
|
|
34
|
+
|
|
35
|
+
if (descriptor === undefined) {
|
|
36
|
+
return yield* failCommand(
|
|
37
|
+
context,
|
|
38
|
+
"nyc-dot",
|
|
39
|
+
"unsupported-dataset",
|
|
40
|
+
`Unsupported NYC DOT dataset: ${config.name}`,
|
|
41
|
+
["nyc-dot", "dataset", "info"]
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return yield* writeSuccess(context, "nyc-dot", descriptor)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
export const handleNycDotQuery = (config: {
|
|
49
|
+
readonly name: string
|
|
50
|
+
readonly select: string
|
|
51
|
+
readonly dryRun: boolean
|
|
52
|
+
}) =>
|
|
53
|
+
Effect.gen(function* () {
|
|
54
|
+
const context = yield* CliCommandContext
|
|
55
|
+
const tokens = ["nyc-dot", "dataset", "query"]
|
|
56
|
+
const descriptor = findNycDotDataset(config.name)
|
|
57
|
+
|
|
58
|
+
if (descriptor === undefined) {
|
|
59
|
+
return yield* failCommand(
|
|
60
|
+
context,
|
|
61
|
+
"nyc-dot",
|
|
62
|
+
"unsupported-dataset",
|
|
63
|
+
`Unsupported NYC DOT dataset: ${config.name}`,
|
|
64
|
+
tokens
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const request = {
|
|
69
|
+
name: config.name,
|
|
70
|
+
query: soqlFromSelect(config.select)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (config.dryRun) {
|
|
74
|
+
return yield* writeSoda3QueryDryRun({
|
|
75
|
+
context,
|
|
76
|
+
apiFamily: "nyc-dot",
|
|
77
|
+
tokens,
|
|
78
|
+
domain: nycDotOpenDataDomain,
|
|
79
|
+
datasetId: String(descriptor.id),
|
|
80
|
+
query: request.query,
|
|
81
|
+
extra: {
|
|
82
|
+
dataset: descriptor
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return yield* runEffect(
|
|
88
|
+
context,
|
|
89
|
+
"nyc-dot",
|
|
90
|
+
tokens,
|
|
91
|
+
queryNycDotDataset(request).pipe(Effect.provide(context.runtime.soda3Layer))
|
|
92
|
+
)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
export const handleNycDotExport = (config: {
|
|
96
|
+
readonly name: string
|
|
97
|
+
readonly format: Soda3ExportFormat
|
|
98
|
+
readonly output: string
|
|
99
|
+
readonly dryRun: boolean
|
|
100
|
+
}) =>
|
|
101
|
+
Effect.gen(function* () {
|
|
102
|
+
const context = yield* CliCommandContext
|
|
103
|
+
const tokens = ["nyc-dot", "dataset", "export"]
|
|
104
|
+
const descriptor = findNycDotDataset(config.name)
|
|
105
|
+
|
|
106
|
+
if (descriptor === undefined) {
|
|
107
|
+
return yield* failCommand(
|
|
108
|
+
context,
|
|
109
|
+
"nyc-dot",
|
|
110
|
+
"unsupported-dataset",
|
|
111
|
+
`Unsupported NYC DOT dataset: ${config.name}`,
|
|
112
|
+
tokens
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const request = {
|
|
117
|
+
name: config.name,
|
|
118
|
+
format: config.format
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (config.dryRun) {
|
|
122
|
+
return yield* writeSoda3ExportDryRun({
|
|
123
|
+
context,
|
|
124
|
+
apiFamily: "nyc-dot",
|
|
125
|
+
tokens,
|
|
126
|
+
domain: nycDotOpenDataDomain,
|
|
127
|
+
datasetId: String(descriptor.id),
|
|
128
|
+
format: request.format,
|
|
129
|
+
output: config.output,
|
|
130
|
+
extra: {
|
|
131
|
+
dataset: descriptor
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return yield* runEffect(
|
|
137
|
+
context,
|
|
138
|
+
"nyc-dot",
|
|
139
|
+
tokens,
|
|
140
|
+
Effect.gen(function* () {
|
|
141
|
+
const response = yield* exportNycDotDataset(request).pipe(
|
|
142
|
+
Effect.provide(context.runtime.soda3Layer)
|
|
143
|
+
)
|
|
144
|
+
return yield* writeResponseToFile(config.output, response)
|
|
145
|
+
})
|
|
146
|
+
)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
const nycDotDatasetListCommand = Command.make("list", {}, handleNycDotList).pipe(
|
|
150
|
+
Command.withShortDescription("List curated NYC DOT dataset descriptors.")
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
const nycDotDatasetInfoCommand = Command.make(
|
|
154
|
+
"info",
|
|
155
|
+
{
|
|
156
|
+
name: Flag.string("name").pipe(
|
|
157
|
+
Flag.withDescription("Known NYC DOT dataset name, such as traffic-speeds.")
|
|
158
|
+
)
|
|
159
|
+
},
|
|
160
|
+
handleNycDotInfo
|
|
161
|
+
).pipe(Command.withShortDescription("Show NYC DOT dataset metadata."))
|
|
162
|
+
|
|
163
|
+
const nycDotDatasetQueryCommand = Command.make(
|
|
164
|
+
"query",
|
|
165
|
+
{
|
|
166
|
+
name: Flag.string("name").pipe(
|
|
167
|
+
Flag.withDescription("Known NYC DOT dataset name, such as traffic-speeds.")
|
|
168
|
+
),
|
|
169
|
+
select: Flag.string("select").pipe(
|
|
170
|
+
Flag.withDescription("SoQL select expression or full SELECT query.")
|
|
171
|
+
),
|
|
172
|
+
dryRun: Flag.boolean("dry-run").pipe(
|
|
173
|
+
Flag.withDescription("Print the planned request without contacting the provider.")
|
|
174
|
+
)
|
|
175
|
+
},
|
|
176
|
+
handleNycDotQuery
|
|
177
|
+
).pipe(Command.withShortDescription("Run a NYC DOT SODA3 query."))
|
|
178
|
+
|
|
179
|
+
const nycDotDatasetExportCommand = Command.make(
|
|
180
|
+
"export",
|
|
181
|
+
{
|
|
182
|
+
name: Flag.string("name").pipe(
|
|
183
|
+
Flag.withDescription("Known NYC DOT dataset name, such as traffic-speeds.")
|
|
184
|
+
),
|
|
185
|
+
format: Flag.choice("format", ["csv", "json", "geojson"]).pipe(
|
|
186
|
+
Flag.withDefault("csv"),
|
|
187
|
+
Flag.withDescription("Export format. Defaults to csv.")
|
|
188
|
+
),
|
|
189
|
+
output: Flag.string("output").pipe(
|
|
190
|
+
Flag.withDescription("Destination file path for the downloaded response body.")
|
|
191
|
+
),
|
|
192
|
+
dryRun: Flag.boolean("dry-run").pipe(
|
|
193
|
+
Flag.withDescription("Print the planned request without contacting the provider.")
|
|
194
|
+
)
|
|
195
|
+
},
|
|
196
|
+
handleNycDotExport
|
|
197
|
+
).pipe(Command.withShortDescription("Export a NYC DOT dataset."))
|
|
198
|
+
|
|
199
|
+
const nycDotDatasetCommand = Command.make("dataset").pipe(
|
|
200
|
+
Command.withShortDescription("NYC DOT dataset commands."),
|
|
201
|
+
Command.withSubcommands([
|
|
202
|
+
nycDotDatasetListCommand,
|
|
203
|
+
nycDotDatasetInfoCommand,
|
|
204
|
+
nycDotDatasetQueryCommand,
|
|
205
|
+
nycDotDatasetExportCommand
|
|
206
|
+
])
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
export const nycDotCommand = Command.make("nyc-dot").pipe(
|
|
210
|
+
Command.withShortDescription("NYC DOT commands."),
|
|
211
|
+
Command.withSubcommands([nycDotDatasetCommand])
|
|
212
|
+
)
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import type { Soda3ExportFormat } from "@nyc-transit-kit/contracts/soda3"
|
|
2
|
+
import { searchNycOpenDataCatalog } from "@nyc-transit-kit/nyc-open-data/catalog"
|
|
3
|
+
import { exportNycOpenDataDataset } from "@nyc-transit-kit/nyc-open-data/client"
|
|
4
|
+
import {
|
|
5
|
+
defaultDomain,
|
|
6
|
+
findNycOpenDataDataset,
|
|
7
|
+
knownNycOpenDataDatasets
|
|
8
|
+
} from "@nyc-transit-kit/nyc-open-data/descriptors"
|
|
9
|
+
import { queryNycOpenDataDataset } from "@nyc-transit-kit/nyc-open-data/query"
|
|
10
|
+
import { buildCatalogSearchUrl } from "@nyc-transit-kit/soda3/endpoints"
|
|
11
|
+
import * as Effect from "effect/Effect"
|
|
12
|
+
import * as Command from "effect/unstable/cli/Command"
|
|
13
|
+
import * as Flag from "effect/unstable/cli/Flag"
|
|
14
|
+
import { runEffect, soqlFromSelect, writeSuccess } from "./shared"
|
|
15
|
+
import { writeResponseToFile, writeSoda3ExportDryRun, writeSoda3QueryDryRun } from "./soda3-shared"
|
|
16
|
+
import { CliCommandContext } from "./types"
|
|
17
|
+
|
|
18
|
+
export const nycOpenDataCommandPaths: ReadonlyArray<ReadonlyArray<string>> = [
|
|
19
|
+
["nyc-open-data", "catalog", "search"],
|
|
20
|
+
["nyc-open-data", "dataset", "list"],
|
|
21
|
+
["nyc-open-data", "dataset", "info"],
|
|
22
|
+
["nyc-open-data", "dataset", "query"],
|
|
23
|
+
["nyc-open-data", "dataset", "export"]
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
export const handleNycOpenDataCatalogSearch = (config: {
|
|
27
|
+
readonly query: string
|
|
28
|
+
readonly limit: number
|
|
29
|
+
readonly dryRun: boolean
|
|
30
|
+
}) =>
|
|
31
|
+
Effect.gen(function* () {
|
|
32
|
+
const context = yield* CliCommandContext
|
|
33
|
+
const tokens = ["nyc-open-data", "catalog", "search"]
|
|
34
|
+
const request = {
|
|
35
|
+
query: config.query,
|
|
36
|
+
limit: config.limit
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (config.dryRun) {
|
|
40
|
+
return yield* runEffect(
|
|
41
|
+
context,
|
|
42
|
+
"nyc-open-data",
|
|
43
|
+
tokens,
|
|
44
|
+
buildCatalogSearchUrl({
|
|
45
|
+
domain: defaultDomain,
|
|
46
|
+
...request
|
|
47
|
+
}).pipe(
|
|
48
|
+
Effect.map((url) => ({
|
|
49
|
+
dryRun: true,
|
|
50
|
+
method: "GET",
|
|
51
|
+
url: url.toString()
|
|
52
|
+
}))
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return yield* runEffect(
|
|
58
|
+
context,
|
|
59
|
+
"nyc-open-data",
|
|
60
|
+
tokens,
|
|
61
|
+
searchNycOpenDataCatalog(request).pipe(Effect.provide(context.runtime.soda3Layer))
|
|
62
|
+
)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
export const handleNycOpenDataList = () =>
|
|
66
|
+
Effect.gen(function* () {
|
|
67
|
+
const context = yield* CliCommandContext
|
|
68
|
+
return yield* writeSuccess(context, "nyc-open-data", {
|
|
69
|
+
datasets: knownNycOpenDataDatasets
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
export const handleNycOpenDataInfo = (config: { readonly datasetId: string }) =>
|
|
74
|
+
Effect.gen(function* () {
|
|
75
|
+
const context = yield* CliCommandContext
|
|
76
|
+
const descriptor = findNycOpenDataDataset(config.datasetId)
|
|
77
|
+
|
|
78
|
+
return yield* writeSuccess(context, "nyc-open-data", {
|
|
79
|
+
known: descriptor !== undefined,
|
|
80
|
+
descriptor:
|
|
81
|
+
descriptor === undefined
|
|
82
|
+
? {
|
|
83
|
+
id: config.datasetId,
|
|
84
|
+
domain: defaultDomain,
|
|
85
|
+
backing: "socrata"
|
|
86
|
+
}
|
|
87
|
+
: descriptor
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
export const handleNycOpenDataQuery = (config: {
|
|
92
|
+
readonly datasetId: string
|
|
93
|
+
readonly select: string
|
|
94
|
+
readonly dryRun: boolean
|
|
95
|
+
}) =>
|
|
96
|
+
Effect.gen(function* () {
|
|
97
|
+
const context = yield* CliCommandContext
|
|
98
|
+
const tokens = ["nyc-open-data", "dataset", "query"]
|
|
99
|
+
const request = {
|
|
100
|
+
datasetId: config.datasetId,
|
|
101
|
+
query: soqlFromSelect(config.select)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (config.dryRun) {
|
|
105
|
+
return yield* writeSoda3QueryDryRun({
|
|
106
|
+
context,
|
|
107
|
+
apiFamily: "nyc-open-data",
|
|
108
|
+
tokens,
|
|
109
|
+
domain: defaultDomain,
|
|
110
|
+
...request
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return yield* runEffect(
|
|
115
|
+
context,
|
|
116
|
+
"nyc-open-data",
|
|
117
|
+
tokens,
|
|
118
|
+
queryNycOpenDataDataset(request).pipe(Effect.provide(context.runtime.soda3Layer))
|
|
119
|
+
)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
export const handleNycOpenDataExport = (config: {
|
|
123
|
+
readonly datasetId: string
|
|
124
|
+
readonly format: Soda3ExportFormat
|
|
125
|
+
readonly output: string
|
|
126
|
+
readonly dryRun: boolean
|
|
127
|
+
}) =>
|
|
128
|
+
Effect.gen(function* () {
|
|
129
|
+
const context = yield* CliCommandContext
|
|
130
|
+
const tokens = ["nyc-open-data", "dataset", "export"]
|
|
131
|
+
const request = {
|
|
132
|
+
datasetId: config.datasetId,
|
|
133
|
+
format: config.format
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (config.dryRun) {
|
|
137
|
+
return yield* writeSoda3ExportDryRun({
|
|
138
|
+
context,
|
|
139
|
+
apiFamily: "nyc-open-data",
|
|
140
|
+
tokens,
|
|
141
|
+
domain: defaultDomain,
|
|
142
|
+
output: config.output,
|
|
143
|
+
...request
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return yield* runEffect(
|
|
148
|
+
context,
|
|
149
|
+
"nyc-open-data",
|
|
150
|
+
tokens,
|
|
151
|
+
Effect.gen(function* () {
|
|
152
|
+
const response = yield* exportNycOpenDataDataset(request).pipe(
|
|
153
|
+
Effect.provide(context.runtime.soda3Layer)
|
|
154
|
+
)
|
|
155
|
+
return yield* writeResponseToFile(config.output, response)
|
|
156
|
+
})
|
|
157
|
+
)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
const nycOpenDataCatalogSearchCommand = Command.make(
|
|
161
|
+
"search",
|
|
162
|
+
{
|
|
163
|
+
query: Flag.string("query").pipe(Flag.withDescription("Search text for NYC Open Data.")),
|
|
164
|
+
limit: Flag.integer("limit").pipe(
|
|
165
|
+
Flag.withDefault(10),
|
|
166
|
+
Flag.withDescription("Maximum catalog results to request. Defaults to 10.")
|
|
167
|
+
),
|
|
168
|
+
dryRun: Flag.boolean("dry-run").pipe(
|
|
169
|
+
Flag.withDescription("Print the planned request without contacting the provider.")
|
|
170
|
+
)
|
|
171
|
+
},
|
|
172
|
+
handleNycOpenDataCatalogSearch
|
|
173
|
+
).pipe(Command.withShortDescription("Search the NYC Open Data catalog."))
|
|
174
|
+
|
|
175
|
+
const nycOpenDataCatalogCommand = Command.make("catalog").pipe(
|
|
176
|
+
Command.withShortDescription("NYC Open Data catalog commands."),
|
|
177
|
+
Command.withSubcommands([nycOpenDataCatalogSearchCommand])
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
const nycOpenDataDatasetListCommand = Command.make("list", {}, handleNycOpenDataList).pipe(
|
|
181
|
+
Command.withShortDescription("List curated NYC Open Data descriptors.")
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
const nycOpenDataDatasetInfoCommand = Command.make(
|
|
185
|
+
"info",
|
|
186
|
+
{
|
|
187
|
+
datasetId: Flag.string("dataset").pipe(
|
|
188
|
+
Flag.withDescription("NYC Open Data Socrata dataset id.")
|
|
189
|
+
)
|
|
190
|
+
},
|
|
191
|
+
handleNycOpenDataInfo
|
|
192
|
+
).pipe(Command.withShortDescription("Show NYC Open Data dataset metadata."))
|
|
193
|
+
|
|
194
|
+
const nycOpenDataDatasetQueryCommand = Command.make(
|
|
195
|
+
"query",
|
|
196
|
+
{
|
|
197
|
+
datasetId: Flag.string("dataset").pipe(
|
|
198
|
+
Flag.withDescription("NYC Open Data Socrata dataset id.")
|
|
199
|
+
),
|
|
200
|
+
select: Flag.string("select").pipe(
|
|
201
|
+
Flag.withDescription("SoQL select expression or full SELECT query.")
|
|
202
|
+
),
|
|
203
|
+
dryRun: Flag.boolean("dry-run").pipe(
|
|
204
|
+
Flag.withDescription("Print the planned request without contacting the provider.")
|
|
205
|
+
)
|
|
206
|
+
},
|
|
207
|
+
handleNycOpenDataQuery
|
|
208
|
+
).pipe(Command.withShortDescription("Run a NYC Open Data SODA3 query."))
|
|
209
|
+
|
|
210
|
+
const nycOpenDataDatasetExportCommand = Command.make(
|
|
211
|
+
"export",
|
|
212
|
+
{
|
|
213
|
+
datasetId: Flag.string("dataset").pipe(
|
|
214
|
+
Flag.withDescription("NYC Open Data Socrata dataset id.")
|
|
215
|
+
),
|
|
216
|
+
format: Flag.choice("format", ["csv", "json", "geojson"]).pipe(
|
|
217
|
+
Flag.withDefault("csv"),
|
|
218
|
+
Flag.withDescription("Export format. Defaults to csv.")
|
|
219
|
+
),
|
|
220
|
+
output: Flag.string("output").pipe(
|
|
221
|
+
Flag.withDescription("Destination file path for the downloaded response body.")
|
|
222
|
+
),
|
|
223
|
+
dryRun: Flag.boolean("dry-run").pipe(
|
|
224
|
+
Flag.withDescription("Print the planned request without contacting the provider.")
|
|
225
|
+
)
|
|
226
|
+
},
|
|
227
|
+
handleNycOpenDataExport
|
|
228
|
+
).pipe(Command.withShortDescription("Export a NYC Open Data dataset."))
|
|
229
|
+
|
|
230
|
+
const nycOpenDataDatasetCommand = Command.make("dataset").pipe(
|
|
231
|
+
Command.withShortDescription("NYC Open Data dataset commands."),
|
|
232
|
+
Command.withSubcommands([
|
|
233
|
+
nycOpenDataDatasetListCommand,
|
|
234
|
+
nycOpenDataDatasetInfoCommand,
|
|
235
|
+
nycOpenDataDatasetQueryCommand,
|
|
236
|
+
nycOpenDataDatasetExportCommand
|
|
237
|
+
])
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
export const nycOpenDataCommand = Command.make("nyc-open-data").pipe(
|
|
241
|
+
Command.withShortDescription("NYC Open Data commands."),
|
|
242
|
+
Command.withSubcommands([nycOpenDataCatalogCommand, nycOpenDataDatasetCommand])
|
|
243
|
+
)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect"
|
|
2
|
+
import * as Command from "effect/unstable/cli/Command"
|
|
3
|
+
import * as Flag from "effect/unstable/cli/Flag"
|
|
4
|
+
import { localBinaryName } from "../index"
|
|
5
|
+
import { catalogCommand } from "./catalog"
|
|
6
|
+
import { mtaCommand } from "./mta"
|
|
7
|
+
import { nycDotCommand } from "./nyc-dot"
|
|
8
|
+
import { nycOpenDataCommand } from "./nyc-open-data"
|
|
9
|
+
import { failCommand } from "./shared"
|
|
10
|
+
import { socrataCommand } from "./socrata"
|
|
11
|
+
import { CliCommandContext } from "./types"
|
|
12
|
+
|
|
13
|
+
const handleMissingCommand = () =>
|
|
14
|
+
Effect.gen(function* () {
|
|
15
|
+
const context = yield* CliCommandContext
|
|
16
|
+
return yield* failCommand(
|
|
17
|
+
context,
|
|
18
|
+
"cli",
|
|
19
|
+
"missing-command",
|
|
20
|
+
`Run ${localBinaryName} --version --json for the current scaffold contract.`,
|
|
21
|
+
[]
|
|
22
|
+
)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
export const rootCommand = Command.make("ntk", {}, handleMissingCommand).pipe(
|
|
26
|
+
Command.withSharedFlags({
|
|
27
|
+
json: Flag.boolean("json").pipe(
|
|
28
|
+
Flag.withDescription("Write the stable JSON envelope instead of human output.")
|
|
29
|
+
)
|
|
30
|
+
}),
|
|
31
|
+
Command.withSubcommands([
|
|
32
|
+
socrataCommand,
|
|
33
|
+
catalogCommand,
|
|
34
|
+
mtaCommand,
|
|
35
|
+
nycOpenDataCommand,
|
|
36
|
+
nycDotCommand
|
|
37
|
+
])
|
|
38
|
+
)
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect"
|
|
2
|
+
import { errorEnvelope, successEnvelope } from "../output"
|
|
3
|
+
import type { ApiFamily, CommandContext, CommandResult } from "./types"
|
|
4
|
+
|
|
5
|
+
export const writeJson = (output: unknown) => {
|
|
6
|
+
console.log(JSON.stringify(output))
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const commandLabel = (tokens: ReadonlyArray<string>) =>
|
|
10
|
+
tokens.length > 0 ? tokens.join(" ") : "(missing command)"
|
|
11
|
+
|
|
12
|
+
export const apiFamilyFor = (tokens: ReadonlyArray<string>): ApiFamily => {
|
|
13
|
+
switch (tokens[0]) {
|
|
14
|
+
case "socrata":
|
|
15
|
+
case "catalog":
|
|
16
|
+
return "socrata"
|
|
17
|
+
case "mta":
|
|
18
|
+
return "mta"
|
|
19
|
+
case "nyc-open-data":
|
|
20
|
+
return "nyc-open-data"
|
|
21
|
+
case "nyc-dot":
|
|
22
|
+
return "nyc-dot"
|
|
23
|
+
default:
|
|
24
|
+
return "cli"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const failCommand = (
|
|
29
|
+
context: CommandContext,
|
|
30
|
+
apiFamily: ApiFamily,
|
|
31
|
+
code: string,
|
|
32
|
+
message: string,
|
|
33
|
+
tokens: ReadonlyArray<string>,
|
|
34
|
+
options?: {
|
|
35
|
+
readonly provider?: string
|
|
36
|
+
readonly retryable?: boolean
|
|
37
|
+
}
|
|
38
|
+
) =>
|
|
39
|
+
Effect.sync(() => {
|
|
40
|
+
process.exitCode = 1
|
|
41
|
+
context.writeJson(
|
|
42
|
+
errorEnvelope(apiFamily, {
|
|
43
|
+
code,
|
|
44
|
+
message,
|
|
45
|
+
provider: options?.provider,
|
|
46
|
+
retryable: options?.retryable,
|
|
47
|
+
command: commandLabel(tokens)
|
|
48
|
+
})
|
|
49
|
+
)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
export const writeSuccess = (context: CommandContext, apiFamily: ApiFamily, data: unknown) =>
|
|
53
|
+
Effect.sync(() => {
|
|
54
|
+
context.writeJson(successEnvelope(apiFamily, data))
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
const writeFailure = (
|
|
58
|
+
context: CommandContext,
|
|
59
|
+
apiFamily: ApiFamily,
|
|
60
|
+
code: string,
|
|
61
|
+
message: string,
|
|
62
|
+
tokens: ReadonlyArray<string>
|
|
63
|
+
) =>
|
|
64
|
+
Effect.sync(() => {
|
|
65
|
+
process.exitCode = 1
|
|
66
|
+
context.writeJson(
|
|
67
|
+
errorEnvelope(apiFamily, {
|
|
68
|
+
code,
|
|
69
|
+
message,
|
|
70
|
+
command: commandLabel(tokens)
|
|
71
|
+
})
|
|
72
|
+
)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
export const missingOption = (
|
|
76
|
+
context: CommandContext,
|
|
77
|
+
tokens: ReadonlyArray<string>,
|
|
78
|
+
option: string
|
|
79
|
+
) =>
|
|
80
|
+
failCommand(
|
|
81
|
+
context,
|
|
82
|
+
apiFamilyFor(tokens),
|
|
83
|
+
"missing-option",
|
|
84
|
+
`Missing required option --${option}.`,
|
|
85
|
+
tokens
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
export const soqlFromSelect = (select: string) =>
|
|
89
|
+
select.trim().toUpperCase().startsWith("SELECT ") ? select : `SELECT ${select}`
|
|
90
|
+
|
|
91
|
+
const taggedMessage = (error: unknown) => {
|
|
92
|
+
if (typeof error === "object" && error !== null && "message" in error) {
|
|
93
|
+
const message = error.message
|
|
94
|
+
if (typeof message === "string") {
|
|
95
|
+
return message
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return "Command failed."
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const taggedCode = (error: unknown) => {
|
|
103
|
+
if (typeof error === "object" && error !== null && "_tag" in error) {
|
|
104
|
+
const tag = error._tag
|
|
105
|
+
if (typeof tag === "string") {
|
|
106
|
+
return tag
|
|
107
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
108
|
+
.replace(/_/g, "-")
|
|
109
|
+
.toLowerCase()
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return "command-failed"
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export const runEffect = <A>(
|
|
117
|
+
context: CommandContext,
|
|
118
|
+
apiFamily: ApiFamily,
|
|
119
|
+
tokens: ReadonlyArray<string>,
|
|
120
|
+
effect: Effect.Effect<A, unknown>
|
|
121
|
+
) =>
|
|
122
|
+
effect.pipe(
|
|
123
|
+
Effect.match({
|
|
124
|
+
onFailure: (error): CommandResult<A> => ({
|
|
125
|
+
_tag: "Failure",
|
|
126
|
+
error
|
|
127
|
+
}),
|
|
128
|
+
onSuccess: (data): CommandResult<A> => ({
|
|
129
|
+
_tag: "Success",
|
|
130
|
+
data
|
|
131
|
+
})
|
|
132
|
+
}),
|
|
133
|
+
Effect.flatMap((result) =>
|
|
134
|
+
result._tag === "Success"
|
|
135
|
+
? writeSuccess(context, apiFamily, result.data)
|
|
136
|
+
: writeFailure(
|
|
137
|
+
context,
|
|
138
|
+
apiFamily,
|
|
139
|
+
taggedCode(result.error),
|
|
140
|
+
taggedMessage(result.error),
|
|
141
|
+
tokens
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
)
|