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 +24 -0
- package/dist/index.mjs +283 -0
- package/package.json +18 -15
- package/src/commands/catalog.ts +48 -0
- package/src/commands/namespace.ts +75 -0
- package/src/commands/table.ts +111 -0
- package/src/index.ts +12 -6
- package/src/utils/iceberg.ts +58 -0
- package/src/utils/json.ts +48 -0
- package/src/utils/logger.ts +67 -0
- package/src/utils/options.ts +17 -31
- package/dist/index.js +0 -855
- package/src/commands/cluster/index.ts +0 -25
- package/src/commands/cluster/namespace/create.ts +0 -314
- package/src/commands/cluster/namespace/delete.ts +0 -54
- package/src/commands/cluster/namespace/get.ts +0 -46
- package/src/commands/cluster/namespace/list.ts +0 -53
- package/src/commands/cluster/table/create.ts +0 -279
- package/src/commands/cluster/table/delete.ts +0 -54
- package/src/commands/cluster/table/get.ts +0 -62
- package/src/commands/cluster/table/list.ts +0 -61
- package/src/commands/dev.ts +0 -136
- package/src/commands/sql.ts +0 -90
- package/src/utils/client.ts +0 -6
- package/src/utils/docker.ts +0 -137
- package/src/utils/wings.ts +0 -124
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
|
-
"repository": "useairfoil/
|
|
3
|
+
"version": "0.4.6",
|
|
4
|
+
"repository": "useairfoil/connector-kit",
|
|
5
5
|
"bin": {
|
|
6
|
-
"airfoil": "./dist/index.
|
|
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.
|
|
14
|
+
"main": "./dist/index.mjs",
|
|
15
15
|
"exports": {
|
|
16
|
-
".": "./dist/index.
|
|
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
|
-
"@
|
|
25
|
-
"@
|
|
26
|
-
"@useairfoil/
|
|
27
|
-
"@useairfoil/wings": "0.4.
|
|
28
|
-
"
|
|
29
|
-
"
|
|
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
|
-
"
|
|
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/
|
|
5
|
-
import { FetchHttpClient } from "effect/
|
|
4
|
+
import { Command } from "effect/cli";
|
|
5
|
+
import { FetchHttpClient } from "effect/http";
|
|
6
6
|
|
|
7
7
|
import packageJson from "../package.json";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
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([
|
|
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
|
);
|