airfoil 0.4.4 → 0.4.6

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/README.md CHANGED
@@ -1 +1,25 @@
1
1
  # Airfoil CLI
2
+
3
+ This is the CLI for Wings catalogs, namespaces, and tables.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pnpm add -g airfoil
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ airfoil --help
15
+ airfoil catalog get analytics
16
+ airfoil namespace list --catalog analytics
17
+ ```
18
+
19
+ ## Configuration
20
+
21
+ | Value | Required | Default |
22
+ | -------------------------------- | -------------------------------- | ----------------------- |
23
+ | `--uri` | no | `http://localhost:7777` |
24
+ | `--catalog` or `AIRFOIL_CATALOG` | for namespace and table commands | none |
25
+ | `--output` | no | `default` |
package/dist/index.mjs ADDED
@@ -0,0 +1,283 @@
1
+ #!/usr/bin/env node
2
+ import { NodeRuntime, NodeServices } from "@effect/platform-node";
3
+ import { Cause, Config, Console, Effect, Formatter, Layer, Logger, Option, Result, Runtime, Schema } from "effect";
4
+ import { Argument, CliError, Command, Flag, GlobalFlag } from "effect/cli";
5
+ import { FetchHttpClient } from "effect/http";
6
+ import { CatalogManager } from "@useairfoil/wings";
7
+ import { encode } from "@toon-format/toon";
8
+ //#region package.json
9
+ var version$1 = "0.4.5";
10
+ //#endregion
11
+ //#region src/utils/json.ts
12
+ const RestCatalogConfigJson = Schema.fromJsonString(CatalogManager.RestCatalogConfig);
13
+ const DefaultValue = Schema.Union([
14
+ Schema.Boolean,
15
+ Schema.Number,
16
+ Schema.String
17
+ ]);
18
+ const IcebergTypeSchema = Schema.suspend(() => Schema.Union([
19
+ Schema.String,
20
+ Schema.Struct({
21
+ type: Schema.Literal("struct"),
22
+ fields: Schema.mutable(Schema.Array(TableFieldSchema))
23
+ }),
24
+ Schema.Struct({
25
+ type: Schema.Literal("list"),
26
+ "element-id": Schema.Int,
27
+ element: IcebergTypeSchema,
28
+ "element-required": Schema.Boolean
29
+ }),
30
+ Schema.Struct({
31
+ type: Schema.Literal("map"),
32
+ "key-id": Schema.Int,
33
+ key: IcebergTypeSchema,
34
+ "value-id": Schema.Int,
35
+ value: IcebergTypeSchema,
36
+ "value-required": Schema.Boolean
37
+ })
38
+ ]));
39
+ const TableFieldSchema = Schema.Struct({
40
+ id: Schema.Int,
41
+ name: Schema.NonEmptyString,
42
+ type: IcebergTypeSchema,
43
+ required: Schema.Boolean,
44
+ doc: Schema.optional(Schema.String),
45
+ "initial-default": Schema.optional(DefaultValue),
46
+ "write-default": Schema.optional(DefaultValue)
47
+ });
48
+ const TableFieldsJson = Schema.fromJsonString(Schema.mutable(Schema.Array(TableFieldSchema)));
49
+ const parseRestCatalogConfig = Schema.decodeUnknownEffect(RestCatalogConfigJson);
50
+ const parseTableFields = Schema.decodeUnknownEffect(TableFieldsJson);
51
+ //#endregion
52
+ //#region src/utils/options.ts
53
+ const WingsUri = GlobalFlag.Setting("uri")({ flag: Flag.String("uri").pipe(Flag.withDefault("http://localhost:7777"), Flag.withDescription("Server URI")) });
54
+ const Output = GlobalFlag.Setting("output")({ flag: Flag.Literals("output", ["default", "json"]).pipe(Flag.withDefault("default"), Flag.withDescription("Log output format")) });
55
+ const catalogFlag = Flag.String("catalog").pipe(Flag.withDescription("Catalog ID. Alternatively, use the AIRFOIL_CATALOG environment variable"), Flag.withFallbackConfig(Config.NonEmptyString("AIRFOIL_CATALOG")));
56
+ //#endregion
57
+ //#region src/commands/catalog.ts
58
+ const idArgument = Argument.String("id").pipe(Argument.withDescription("Catalog ID"));
59
+ const restFlag = Flag.String("rest").pipe(Flag.withDescription("Iceberg REST catalog configuration as JSON"));
60
+ const createCommand$2 = Command.make("create", {
61
+ id: idArgument,
62
+ rest: restFlag
63
+ }, ({ id, rest }) => Effect.gen(function* () {
64
+ const baseUrl = yield* WingsUri;
65
+ const manager = yield* CatalogManager.make({ baseUrl });
66
+ const config = yield* parseRestCatalogConfig(rest);
67
+ const catalog = yield* manager.createCatalog({
68
+ id,
69
+ rest: config
70
+ });
71
+ yield* Effect.log(catalog);
72
+ })).pipe(Command.withDescription("Create a new catalog"));
73
+ const getCommand$2 = Command.make("get", { id: idArgument }, ({ id }) => Effect.gen(function* () {
74
+ const baseUrl = yield* WingsUri;
75
+ const catalog = yield* (yield* CatalogManager.make({ baseUrl })).getCatalog(id);
76
+ yield* Effect.log(catalog);
77
+ })).pipe(Command.withDescription("Get a catalog"));
78
+ const deleteCommand$2 = Command.make("delete", { id: idArgument }, ({ id }) => Effect.gen(function* () {
79
+ const baseUrl = yield* WingsUri;
80
+ yield* (yield* CatalogManager.make({ baseUrl })).deleteCatalog(id);
81
+ yield* Effect.log({ id });
82
+ })).pipe(Command.withDescription("Delete a catalog"));
83
+ const catalogCommand = Command.make("catalog").pipe(Command.withDescription("Manage catalogs"), Command.withSubcommands([
84
+ createCommand$2,
85
+ getCommand$2,
86
+ deleteCommand$2
87
+ ]));
88
+ //#endregion
89
+ //#region src/utils/iceberg.ts
90
+ const NamespaceSegments = Schema.NonEmptyArray(Schema.NonEmptyString);
91
+ const TableSegments = Schema.Array(Schema.NonEmptyString).check(Schema.isMinLength(2));
92
+ const parseNamespaceIdentifier = (name) => Schema.decodeUnknownEffect(NamespaceSegments)(name.split(".")).pipe(Effect.map((namespace) => ({ namespace: Array.from(namespace) })));
93
+ const parseTableIdentifier = (qualifiedName) => Schema.decodeUnknownEffect(TableSegments)(qualifiedName.split(".")).pipe(Effect.map((segments) => ({
94
+ namespace: segments.slice(0, -1),
95
+ name: segments[segments.length - 1]
96
+ })));
97
+ const getIcebergCatalog = Effect.fn("getIcebergCatalog")(function* (catalogId) {
98
+ const baseUrl = yield* WingsUri;
99
+ return yield* (yield* CatalogManager.make({ baseUrl })).getIcebergCatalog(catalogId);
100
+ });
101
+ const listAllNamespaces = Effect.fn("listAllNamespaces")(function* (iceberg) {
102
+ let page = yield* iceberg.listNamespaces();
103
+ const namespaces = [...page.namespaces];
104
+ while (page.nextPageToken !== void 0) {
105
+ page = yield* iceberg.listNamespaces({ pageToken: page.nextPageToken });
106
+ namespaces.push(...page.namespaces);
107
+ }
108
+ return namespaces;
109
+ });
110
+ const listAllTables = Effect.fn("listAllTables")(function* (iceberg, namespace) {
111
+ let page = yield* iceberg.listTables(namespace);
112
+ const identifiers = [...page.identifiers];
113
+ while (page.nextPageToken !== void 0) {
114
+ page = yield* iceberg.listTables(namespace, { pageToken: page.nextPageToken });
115
+ identifiers.push(...page.identifiers);
116
+ }
117
+ return identifiers;
118
+ });
119
+ //#endregion
120
+ //#region src/commands/namespace.ts
121
+ const namespaceArgument$1 = Argument.String("namespace").pipe(Argument.withDescription("Namespace name. Use dot-separated notation for nested namespaces"));
122
+ const parentFlag = Flag.String("parent").pipe(Flag.withDescription("Parent namespace. Use dot-separated notation for nested namespaces"), Flag.optional);
123
+ const createCommand$1 = Command.make("create", {
124
+ namespace: namespaceArgument$1,
125
+ catalog: catalogFlag
126
+ }, ({ namespace, catalog }) => Effect.gen(function* () {
127
+ const id = yield* parseNamespaceIdentifier(namespace);
128
+ const created = yield* (yield* getIcebergCatalog(catalog)).createNamespace(id);
129
+ yield* Effect.log(created);
130
+ })).pipe(Command.withDescription("Create a new namespace"));
131
+ const getCommand$1 = Command.make("get", {
132
+ namespace: namespaceArgument$1,
133
+ catalog: catalogFlag
134
+ }, ({ namespace, catalog }) => Effect.gen(function* () {
135
+ const id = yield* parseNamespaceIdentifier(namespace);
136
+ const metadata = yield* (yield* getIcebergCatalog(catalog)).loadNamespaceMetadata(id);
137
+ yield* Effect.log({
138
+ ...id,
139
+ ...metadata
140
+ });
141
+ })).pipe(Command.withDescription("Get a namespace"));
142
+ const deleteCommand$1 = Command.make("delete", {
143
+ namespace: namespaceArgument$1,
144
+ catalog: catalogFlag
145
+ }, ({ namespace, catalog }) => Effect.gen(function* () {
146
+ const id = yield* parseNamespaceIdentifier(namespace);
147
+ yield* (yield* getIcebergCatalog(catalog)).dropNamespace(id);
148
+ yield* Effect.log(id);
149
+ })).pipe(Command.withDescription("Delete a namespace"));
150
+ const listCommand$1 = Command.make("list", {
151
+ parent: parentFlag,
152
+ catalog: catalogFlag
153
+ }, ({ parent, catalog }) => Effect.gen(function* () {
154
+ const iceberg = yield* getIcebergCatalog(catalog);
155
+ const parentName = Option.getOrUndefined(parent);
156
+ const parentId = parentName === void 0 ? void 0 : yield* parseNamespaceIdentifier(parentName);
157
+ const result = yield* iceberg.listNamespaces(parentId === void 0 ? void 0 : { parent: parentId });
158
+ yield* Effect.log(result);
159
+ })).pipe(Command.withDescription("List namespaces"));
160
+ const namespaceCommand = Command.make("namespace").pipe(Command.withDescription("Manage namespaces"), Command.withSubcommands([
161
+ createCommand$1,
162
+ getCommand$1,
163
+ deleteCommand$1,
164
+ listCommand$1
165
+ ]));
166
+ //#endregion
167
+ //#region src/commands/table.ts
168
+ const tableArgument = Argument.String("table").pipe(Argument.withDescription("Fully qualified table name"));
169
+ const namespaceArgument = Argument.String("namespace").pipe(Argument.withDescription("Namespace name. Use dot-separated notation for nested namespaces"), Argument.optional);
170
+ const allFlag = Flag.Boolean("all").pipe(Flag.withDescription("List tables from all namespaces"), Flag.withDefault(false));
171
+ const schemaFlag = Flag.String("schema").pipe(Flag.withDescription("Iceberg table fields as a JSON array"));
172
+ const createCommand = Command.make("create", {
173
+ table: tableArgument,
174
+ schema: schemaFlag,
175
+ catalog: catalogFlag
176
+ }, ({ table, schema, catalog }) => Effect.gen(function* () {
177
+ const id = yield* parseTableIdentifier(table);
178
+ const fields = yield* parseTableFields(schema);
179
+ const metadata = yield* (yield* getIcebergCatalog(catalog)).createTable({ namespace: id.namespace }, {
180
+ name: id.name,
181
+ schema: {
182
+ type: "struct",
183
+ fields
184
+ }
185
+ });
186
+ yield* Effect.log({
187
+ ...id,
188
+ metadata
189
+ });
190
+ })).pipe(Command.withDescription("Create a new table"));
191
+ const getCommand = Command.make("get", {
192
+ table: tableArgument,
193
+ catalog: catalogFlag
194
+ }, ({ table, catalog }) => Effect.gen(function* () {
195
+ const id = yield* parseTableIdentifier(table);
196
+ const metadata = yield* (yield* getIcebergCatalog(catalog)).loadTable(id);
197
+ yield* Effect.log({
198
+ ...id,
199
+ metadata
200
+ });
201
+ })).pipe(Command.withDescription("Get a table"));
202
+ const deleteCommand = Command.make("delete", {
203
+ table: tableArgument,
204
+ catalog: catalogFlag
205
+ }, ({ table, catalog }) => Effect.gen(function* () {
206
+ const id = yield* parseTableIdentifier(table);
207
+ yield* (yield* getIcebergCatalog(catalog)).dropTable(id);
208
+ yield* Effect.log(id);
209
+ })).pipe(Command.withDescription("Delete a table"));
210
+ const listCommand = Command.make("list", {
211
+ namespace: namespaceArgument,
212
+ all: allFlag,
213
+ catalog: catalogFlag
214
+ }, ({ namespace, all, catalog }) => Effect.gen(function* () {
215
+ const namespaceName = Option.getOrUndefined(namespace);
216
+ if (namespaceName === void 0 && !all) return yield* new CliError.UserError({
217
+ cause: "Missing table list selector",
218
+ userMessage: "Provide a namespace or use --all"
219
+ });
220
+ if (namespaceName !== void 0 && all) return yield* new CliError.UserError({
221
+ cause: "Conflicting table list selectors",
222
+ userMessage: "Provide either a namespace or --all, not both"
223
+ });
224
+ const iceberg = yield* getIcebergCatalog(catalog);
225
+ const tables = namespaceName === void 0 ? (yield* Effect.forEach(yield* listAllNamespaces(iceberg), (namespace) => listAllTables(iceberg, namespace))).flat() : yield* listAllTables(iceberg, yield* parseNamespaceIdentifier(namespaceName));
226
+ yield* Effect.log({
227
+ namespace: namespaceName,
228
+ tables
229
+ });
230
+ })).pipe(Command.withDescription("List tables"));
231
+ const tableCommand = Command.make("table").pipe(Command.withDescription("Manage tables"), Command.withSubcommands([
232
+ createCommand,
233
+ getCommand,
234
+ deleteCommand,
235
+ listCommand
236
+ ]));
237
+ //#endregion
238
+ //#region src/utils/logger.ts
239
+ const decodeJson = Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Unknown));
240
+ const formatStructured = (value, output) => {
241
+ const json = Formatter.formatJson(value);
242
+ return output === "json" ? json : encode(decodeJson(json));
243
+ };
244
+ const messageValue = (message) => Array.isArray(message) && message.length === 1 ? message[0] : message;
245
+ const structuredLogger = (output) => Logger.make(({ message }) => formatStructured(messageValue(message), output)).pipe(Logger.withConsoleLog);
246
+ let errorFormat = "default";
247
+ const OutputLogger = Layer.unwrap(Effect.map(Output, (output) => {
248
+ errorFormat = output;
249
+ return Logger.layer([structuredLogger(output)]);
250
+ }));
251
+ const encodeError = Schema.encodeSync(Schema.Defect());
252
+ const encodeErrorWithStack = Schema.encodeSync(Schema.Defect({ includeStack: true }));
253
+ /**
254
+ * Prints a failed command as `error: message: ... cause: ...` instead of the stack dump
255
+ * `runMain` prints by default.
256
+ *
257
+ * Says nothing about Ctrl+C, or about errors the CLI already printed itself.
258
+ */
259
+ const reportError = (cause) => {
260
+ if (Cause.hasInterruptsOnly(cause)) return Effect.void;
261
+ const error = Cause.squash(cause);
262
+ if (!Runtime.getErrorReported(error)) return Effect.void;
263
+ const defect = Cause.findDefect(cause);
264
+ const stack = Result.isSuccess(defect) && defect.success instanceof Error ? defect.success.stack : void 0;
265
+ if (errorFormat === "json") {
266
+ const encode = stack === void 0 ? encodeError : encodeErrorWithStack;
267
+ return Console.error(formatStructured({ error: encode(error) }, "json"));
268
+ }
269
+ const block = formatStructured({ error: encodeError(error) }, "default");
270
+ return Console.error(stack === void 0 ? block : `${block}\n\n${stack}`);
271
+ };
272
+ //#endregion
273
+ //#region src/index.ts
274
+ const version = version$1;
275
+ const program = Command.make("airfoil", {}, () => Effect.void).pipe(Command.withSubcommands([
276
+ catalogCommand,
277
+ namespaceCommand,
278
+ tableCommand
279
+ ]), Command.provide(OutputLogger), Command.withGlobalFlags([WingsUri, Output]));
280
+ const cli = Command.run(program, { version });
281
+ NodeRuntime.runMain(cli.pipe(Effect.provide(Layer.mergeAll(FetchHttpClient.layer, NodeServices.layer)), Effect.scoped, Effect.tapCause(reportError)), { disableErrorReporting: true });
282
+ //#endregion
283
+ export {};
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "airfoil",
3
- "version": "0.4.4",
4
- "repository": "useairfoil/typescript-sdk",
3
+ "version": "0.4.6",
4
+ "repository": "useairfoil/connector-kit",
5
5
  "bin": {
6
- "airfoil": "./dist/index.js"
6
+ "airfoil": "./dist/index.mjs"
7
7
  },
8
8
  "files": [
9
9
  "dist",
@@ -11,29 +11,32 @@
11
11
  "README.md"
12
12
  ],
13
13
  "type": "module",
14
- "main": "./dist/index.js",
14
+ "main": "./dist/index.mjs",
15
15
  "exports": {
16
- ".": "./dist/index.js"
16
+ ".": "./dist/index.mjs"
17
17
  },
18
18
  "scripts": {
19
19
  "build": "tsdown",
20
+ "test": "vitest",
21
+ "test:ci": "vitest run",
20
22
  "typecheck": "tsc --noEmit",
21
23
  "dev": "tsx src/index.ts"
22
24
  },
23
25
  "dependencies": {
24
- "@clack/prompts": "^0.11.0",
25
- "@effect/platform-node": "^4.0.0-rc.108",
26
- "@useairfoil/flight": "0.4.6",
27
- "@useairfoil/wings": "0.4.4",
28
- "console-table-printer": "^2.15.0",
29
- "effect": "^4.0.0-rc.108",
30
- "nice-grpc": "^2.1.13",
31
- "nice-grpc-common": "^2.0.2"
26
+ "@effect/platform-node": "^4.0.7",
27
+ "@toon-format/toon": "^4.1.1",
28
+ "@useairfoil/effect-iceberg": "0.0.8",
29
+ "@useairfoil/wings": "0.4.7",
30
+ "effect": "^4.0.7",
31
+ "iceberg-js": "^1.0.0"
32
32
  },
33
33
  "devDependencies": {
34
+ "@effect/vitest": "^4.0.7",
34
35
  "@types/node": "^24.2.0",
35
- "tsdown": "^0.15.6",
36
+ "@useairfoil/wings-testing": "0.1.6",
37
+ "tsdown": "^0.23.0",
36
38
  "tsx": "^4.21.0",
37
- "typescript": "^5.9.3"
39
+ "typescript": "^5.9.3",
40
+ "vitest": "^5.0.3"
38
41
  }
39
42
  }
@@ -0,0 +1,48 @@
1
+ import { CatalogManager } from "@useairfoil/wings";
2
+ import { Effect } from "effect";
3
+ import { Argument, Command, Flag } from "effect/cli";
4
+
5
+ import { parseRestCatalogConfig } from "../utils/json";
6
+ import { WingsUri } from "../utils/options";
7
+
8
+ const idArgument = Argument.String("id").pipe(Argument.withDescription("Catalog ID"));
9
+
10
+ const restFlag = Flag.String("rest").pipe(
11
+ Flag.withDescription("Iceberg REST catalog configuration as JSON"),
12
+ );
13
+
14
+ const createCommand = Command.make("create", { id: idArgument, rest: restFlag }, ({ id, rest }) =>
15
+ Effect.gen(function* () {
16
+ const baseUrl = yield* WingsUri;
17
+ const manager = yield* CatalogManager.make({ baseUrl });
18
+ const config = yield* parseRestCatalogConfig(rest);
19
+ const catalog = yield* manager.createCatalog({ id, rest: config });
20
+
21
+ yield* Effect.log(catalog);
22
+ }),
23
+ ).pipe(Command.withDescription("Create a new catalog"));
24
+
25
+ const getCommand = Command.make("get", { id: idArgument }, ({ id }) =>
26
+ Effect.gen(function* () {
27
+ const baseUrl = yield* WingsUri;
28
+ const manager = yield* CatalogManager.make({ baseUrl });
29
+ const catalog = yield* manager.getCatalog(id);
30
+
31
+ yield* Effect.log(catalog);
32
+ }),
33
+ ).pipe(Command.withDescription("Get a catalog"));
34
+
35
+ const deleteCommand = Command.make("delete", { id: idArgument }, ({ id }) =>
36
+ Effect.gen(function* () {
37
+ const baseUrl = yield* WingsUri;
38
+ const manager = yield* CatalogManager.make({ baseUrl });
39
+
40
+ yield* manager.deleteCatalog(id);
41
+ yield* Effect.log({ id });
42
+ }),
43
+ ).pipe(Command.withDescription("Delete a catalog"));
44
+
45
+ export const catalogCommand = Command.make("catalog").pipe(
46
+ Command.withDescription("Manage catalogs"),
47
+ Command.withSubcommands([createCommand, getCommand, deleteCommand]),
48
+ );
@@ -0,0 +1,75 @@
1
+ import { Effect, Option } from "effect";
2
+ import { Argument, Command, Flag } from "effect/cli";
3
+
4
+ import { getIcebergCatalog, parseNamespaceIdentifier } from "../utils/iceberg";
5
+ import { catalogFlag } from "../utils/options";
6
+
7
+ const namespaceArgument = Argument.String("namespace").pipe(
8
+ Argument.withDescription("Namespace name. Use dot-separated notation for nested namespaces"),
9
+ );
10
+
11
+ const parentFlag = Flag.String("parent").pipe(
12
+ Flag.withDescription("Parent namespace. Use dot-separated notation for nested namespaces"),
13
+ Flag.optional,
14
+ );
15
+
16
+ const createCommand = Command.make(
17
+ "create",
18
+ { namespace: namespaceArgument, catalog: catalogFlag },
19
+ ({ namespace, catalog }) =>
20
+ Effect.gen(function* () {
21
+ const id = yield* parseNamespaceIdentifier(namespace);
22
+ const iceberg = yield* getIcebergCatalog(catalog);
23
+ const created = yield* iceberg.createNamespace(id);
24
+
25
+ yield* Effect.log(created);
26
+ }),
27
+ ).pipe(Command.withDescription("Create a new namespace"));
28
+
29
+ const getCommand = Command.make(
30
+ "get",
31
+ { namespace: namespaceArgument, catalog: catalogFlag },
32
+ ({ namespace, catalog }) =>
33
+ Effect.gen(function* () {
34
+ const id = yield* parseNamespaceIdentifier(namespace);
35
+ const iceberg = yield* getIcebergCatalog(catalog);
36
+ const metadata = yield* iceberg.loadNamespaceMetadata(id);
37
+
38
+ yield* Effect.log({ ...id, ...metadata });
39
+ }),
40
+ ).pipe(Command.withDescription("Get a namespace"));
41
+
42
+ const deleteCommand = Command.make(
43
+ "delete",
44
+ { namespace: namespaceArgument, catalog: catalogFlag },
45
+ ({ namespace, catalog }) =>
46
+ Effect.gen(function* () {
47
+ const id = yield* parseNamespaceIdentifier(namespace);
48
+ const iceberg = yield* getIcebergCatalog(catalog);
49
+
50
+ yield* iceberg.dropNamespace(id);
51
+ yield* Effect.log(id);
52
+ }),
53
+ ).pipe(Command.withDescription("Delete a namespace"));
54
+
55
+ const listCommand = Command.make(
56
+ "list",
57
+ { parent: parentFlag, catalog: catalogFlag },
58
+ ({ parent, catalog }) =>
59
+ Effect.gen(function* () {
60
+ const iceberg = yield* getIcebergCatalog(catalog);
61
+ const parentName = Option.getOrUndefined(parent);
62
+ const parentId =
63
+ parentName === undefined ? undefined : yield* parseNamespaceIdentifier(parentName);
64
+ const result = yield* iceberg.listNamespaces(
65
+ parentId === undefined ? undefined : { parent: parentId },
66
+ );
67
+
68
+ yield* Effect.log(result);
69
+ }),
70
+ ).pipe(Command.withDescription("List namespaces"));
71
+
72
+ export const namespaceCommand = Command.make("namespace").pipe(
73
+ Command.withDescription("Manage namespaces"),
74
+ Command.withSubcommands([createCommand, getCommand, deleteCommand, listCommand]),
75
+ );
@@ -0,0 +1,111 @@
1
+ import { Effect, Option } from "effect";
2
+ import { Argument, CliError, Command, Flag } from "effect/cli";
3
+
4
+ import {
5
+ getIcebergCatalog,
6
+ listAllNamespaces,
7
+ listAllTables,
8
+ parseNamespaceIdentifier,
9
+ parseTableIdentifier,
10
+ } from "../utils/iceberg";
11
+ import { parseTableFields } from "../utils/json";
12
+ import { catalogFlag } from "../utils/options";
13
+
14
+ const tableArgument = Argument.String("table").pipe(
15
+ Argument.withDescription("Fully qualified table name"),
16
+ );
17
+
18
+ const namespaceArgument = Argument.String("namespace").pipe(
19
+ Argument.withDescription("Namespace name. Use dot-separated notation for nested namespaces"),
20
+ Argument.optional,
21
+ );
22
+
23
+ const allFlag = Flag.Boolean("all").pipe(
24
+ Flag.withDescription("List tables from all namespaces"),
25
+ Flag.withDefault(false),
26
+ );
27
+
28
+ const schemaFlag = Flag.String("schema").pipe(
29
+ Flag.withDescription("Iceberg table fields as a JSON array"),
30
+ );
31
+
32
+ const createCommand = Command.make(
33
+ "create",
34
+ { table: tableArgument, schema: schemaFlag, catalog: catalogFlag },
35
+ ({ table, schema, catalog }) =>
36
+ Effect.gen(function* () {
37
+ const id = yield* parseTableIdentifier(table);
38
+ const fields = yield* parseTableFields(schema);
39
+ const iceberg = yield* getIcebergCatalog(catalog);
40
+ const metadata = yield* iceberg.createTable(
41
+ { namespace: id.namespace },
42
+ { name: id.name, schema: { type: "struct", fields } },
43
+ );
44
+
45
+ yield* Effect.log({ ...id, metadata });
46
+ }),
47
+ ).pipe(Command.withDescription("Create a new table"));
48
+
49
+ const getCommand = Command.make(
50
+ "get",
51
+ { table: tableArgument, catalog: catalogFlag },
52
+ ({ table, catalog }) =>
53
+ Effect.gen(function* () {
54
+ const id = yield* parseTableIdentifier(table);
55
+ const iceberg = yield* getIcebergCatalog(catalog);
56
+ const metadata = yield* iceberg.loadTable(id);
57
+
58
+ yield* Effect.log({ ...id, metadata });
59
+ }),
60
+ ).pipe(Command.withDescription("Get a table"));
61
+
62
+ const deleteCommand = Command.make(
63
+ "delete",
64
+ { table: tableArgument, catalog: catalogFlag },
65
+ ({ table, catalog }) =>
66
+ Effect.gen(function* () {
67
+ const id = yield* parseTableIdentifier(table);
68
+ const iceberg = yield* getIcebergCatalog(catalog);
69
+
70
+ yield* iceberg.dropTable(id);
71
+ yield* Effect.log(id);
72
+ }),
73
+ ).pipe(Command.withDescription("Delete a table"));
74
+
75
+ const listCommand = Command.make(
76
+ "list",
77
+ { namespace: namespaceArgument, all: allFlag, catalog: catalogFlag },
78
+ ({ namespace, all, catalog }) =>
79
+ Effect.gen(function* () {
80
+ const namespaceName = Option.getOrUndefined(namespace);
81
+
82
+ if (namespaceName === undefined && !all) {
83
+ return yield* new CliError.UserError({
84
+ cause: "Missing table list selector",
85
+ userMessage: "Provide a namespace or use --all",
86
+ });
87
+ }
88
+
89
+ if (namespaceName !== undefined && all) {
90
+ return yield* new CliError.UserError({
91
+ cause: "Conflicting table list selectors",
92
+ userMessage: "Provide either a namespace or --all, not both",
93
+ });
94
+ }
95
+
96
+ const iceberg = yield* getIcebergCatalog(catalog);
97
+ const tables =
98
+ namespaceName === undefined
99
+ ? (yield* Effect.forEach(yield* listAllNamespaces(iceberg), (namespace) =>
100
+ listAllTables(iceberg, namespace),
101
+ )).flat()
102
+ : yield* listAllTables(iceberg, yield* parseNamespaceIdentifier(namespaceName));
103
+
104
+ yield* Effect.log({ namespace: namespaceName, tables });
105
+ }),
106
+ ).pipe(Command.withDescription("List tables"));
107
+
108
+ export const tableCommand = Command.make("table").pipe(
109
+ Command.withDescription("Manage tables"),
110
+ Command.withSubcommands([createCommand, getCommand, deleteCommand, listCommand]),
111
+ );
package/src/index.ts CHANGED
@@ -1,18 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
  import { NodeRuntime, NodeServices } from "@effect/platform-node";
3
3
  import { Effect, Layer } from "effect";
4
- import { Command } from "effect/unstable/cli";
5
- import { FetchHttpClient } from "effect/unstable/http";
4
+ import { Command } from "effect/cli";
5
+ import { FetchHttpClient } from "effect/http";
6
6
 
7
7
  import packageJson from "../package.json";
8
- import { clusterCommand } from "./commands/cluster/index";
9
- import { devCommand } from "./commands/dev";
10
- import { sqlCommand } from "./commands/sql";
8
+ import { catalogCommand } from "./commands/catalog";
9
+ import { namespaceCommand } from "./commands/namespace";
10
+ import { tableCommand } from "./commands/table";
11
+ import { OutputLogger, reportError } from "./utils/logger";
12
+ import { Output, WingsUri } from "./utils/options";
11
13
 
12
14
  const version = packageJson.version;
13
15
 
14
16
  const program = Command.make("airfoil", {}, () => Effect.void).pipe(
15
- Command.withSubcommands([devCommand, sqlCommand, clusterCommand]),
17
+ Command.withSubcommands([catalogCommand, namespaceCommand, tableCommand]),
18
+ Command.provide(OutputLogger),
19
+ Command.withGlobalFlags([WingsUri, Output]),
16
20
  );
17
21
 
18
22
  const cli = Command.run(program, {
@@ -23,5 +27,7 @@ NodeRuntime.runMain(
23
27
  cli.pipe(
24
28
  Effect.provide(Layer.mergeAll(FetchHttpClient.layer, NodeServices.layer)),
25
29
  Effect.scoped,
30
+ Effect.tapCause(reportError),
26
31
  ),
32
+ { disableErrorReporting: true },
27
33
  );