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.
@@ -0,0 +1,58 @@
1
+ import type { IcebergCatalog } from "@useairfoil/effect-iceberg";
2
+
3
+ import { CatalogManager } from "@useairfoil/wings";
4
+ import { Effect, Schema } from "effect";
5
+
6
+ import { WingsUri } from "./options";
7
+
8
+ const NamespaceSegments = Schema.NonEmptyArray(Schema.NonEmptyString);
9
+ const TableSegments = Schema.Array(Schema.NonEmptyString).check(Schema.isMinLength(2));
10
+
11
+ export const parseNamespaceIdentifier = (name: string) =>
12
+ Schema.decodeUnknownEffect(NamespaceSegments)(name.split(".")).pipe(
13
+ Effect.map((namespace) => ({ namespace: Array.from(namespace) })),
14
+ );
15
+
16
+ export const parseTableIdentifier = (qualifiedName: string) =>
17
+ Schema.decodeUnknownEffect(TableSegments)(qualifiedName.split(".")).pipe(
18
+ Effect.map((segments) => ({
19
+ namespace: segments.slice(0, -1),
20
+ name: segments[segments.length - 1]!,
21
+ })),
22
+ );
23
+
24
+ export const getIcebergCatalog = Effect.fn("getIcebergCatalog")(function* (catalogId: string) {
25
+ const baseUrl = yield* WingsUri;
26
+ const manager = yield* CatalogManager.make({ baseUrl });
27
+
28
+ return yield* manager.getIcebergCatalog(catalogId);
29
+ });
30
+
31
+ export const listAllNamespaces = Effect.fn("listAllNamespaces")(function* (
32
+ iceberg: IcebergCatalog.Service,
33
+ ) {
34
+ let page = yield* iceberg.listNamespaces();
35
+ const namespaces = [...page.namespaces];
36
+
37
+ while (page.nextPageToken !== undefined) {
38
+ page = yield* iceberg.listNamespaces({ pageToken: page.nextPageToken });
39
+ namespaces.push(...page.namespaces);
40
+ }
41
+
42
+ return namespaces;
43
+ });
44
+
45
+ export const listAllTables = Effect.fn("listAllTables")(function* (
46
+ iceberg: IcebergCatalog.Service,
47
+ namespace: Parameters<IcebergCatalog.Service["listTables"]>[0],
48
+ ) {
49
+ let page = yield* iceberg.listTables(namespace);
50
+ const identifiers = [...page.identifiers];
51
+
52
+ while (page.nextPageToken !== undefined) {
53
+ page = yield* iceberg.listTables(namespace, { pageToken: page.nextPageToken });
54
+ identifiers.push(...page.identifiers);
55
+ }
56
+
57
+ return identifiers;
58
+ });
@@ -0,0 +1,48 @@
1
+ import type { IcebergType, TableField } from "iceberg-js";
2
+
3
+ import { CatalogManager } from "@useairfoil/wings";
4
+ import { Schema } from "effect";
5
+
6
+ const RestCatalogConfigJson = Schema.fromJsonString(CatalogManager.RestCatalogConfig);
7
+
8
+ const DefaultValue = Schema.Union([Schema.Boolean, Schema.Number, Schema.String]);
9
+
10
+ const IcebergTypeSchema: Schema.Codec<IcebergType> = Schema.suspend(() =>
11
+ Schema.Union([
12
+ Schema.String,
13
+ Schema.Struct({
14
+ type: Schema.Literal("struct"),
15
+ fields: Schema.mutable(Schema.Array(TableFieldSchema)),
16
+ }),
17
+ Schema.Struct({
18
+ type: Schema.Literal("list"),
19
+ "element-id": Schema.Int,
20
+ element: IcebergTypeSchema,
21
+ "element-required": Schema.Boolean,
22
+ }),
23
+ Schema.Struct({
24
+ type: Schema.Literal("map"),
25
+ "key-id": Schema.Int,
26
+ key: IcebergTypeSchema,
27
+ "value-id": Schema.Int,
28
+ value: IcebergTypeSchema,
29
+ "value-required": Schema.Boolean,
30
+ }),
31
+ ]),
32
+ );
33
+
34
+ const TableFieldSchema: Schema.Codec<TableField> = Schema.Struct({
35
+ id: Schema.Int,
36
+ name: Schema.NonEmptyString,
37
+ type: IcebergTypeSchema,
38
+ required: Schema.Boolean,
39
+ doc: Schema.optional(Schema.String),
40
+ "initial-default": Schema.optional(DefaultValue),
41
+ "write-default": Schema.optional(DefaultValue),
42
+ });
43
+
44
+ const TableFieldsJson = Schema.fromJsonString(Schema.mutable(Schema.Array(TableFieldSchema)));
45
+
46
+ export const parseRestCatalogConfig = Schema.decodeUnknownEffect(RestCatalogConfigJson);
47
+
48
+ export const parseTableFields = Schema.decodeUnknownEffect(TableFieldsJson);
@@ -0,0 +1,67 @@
1
+ import { encode } from "@toon-format/toon";
2
+ import { Cause, Console, Effect, Formatter, Layer, Logger, Result, Runtime, Schema } from "effect";
3
+
4
+ import { Output } from "./options";
5
+
6
+ const decodeJson = Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Unknown));
7
+
8
+ export type OutputFormat = "default" | "json";
9
+
10
+ export const formatStructured = (value: unknown, output: OutputFormat) => {
11
+ const json = Formatter.formatJson(value);
12
+
13
+ return output === "json" ? json : encode(decodeJson(json));
14
+ };
15
+
16
+ const messageValue = (message: unknown) =>
17
+ Array.isArray(message) && message.length === 1 ? message[0] : message;
18
+
19
+ const structuredLogger = (output: OutputFormat) =>
20
+ Logger.make<unknown, string>(({ message }) =>
21
+ formatStructured(messageValue(message), output),
22
+ ).pipe(Logger.withConsoleLog);
23
+
24
+ // Errors are reported after the command ends, when its flags are gone. Keep the format here.
25
+ let errorFormat: OutputFormat = "default";
26
+
27
+ export const OutputLogger = Layer.unwrap(
28
+ Effect.map(Output, (output) => {
29
+ errorFormat = output;
30
+
31
+ return Logger.layer([structuredLogger(output)]);
32
+ }),
33
+ );
34
+
35
+ // Turns an error and its causes into plain JSON, so we do not walk the chain ourselves.
36
+ const encodeError = Schema.encodeSync(Schema.Defect());
37
+ const encodeErrorWithStack = Schema.encodeSync(Schema.Defect({ includeStack: true }));
38
+
39
+ /**
40
+ * Prints a failed command as `error: message: ... cause: ...` instead of the stack dump
41
+ * `runMain` prints by default.
42
+ *
43
+ * Says nothing about Ctrl+C, or about errors the CLI already printed itself.
44
+ */
45
+ export const reportError = (cause: Cause.Cause<unknown>) => {
46
+ if (Cause.hasInterruptsOnly(cause)) return Effect.void;
47
+
48
+ const error = Cause.squash(cause);
49
+
50
+ if (!Runtime.getErrorReported(error)) return Effect.void;
51
+
52
+ // Only an unexpected defect is worth a stack. A failure is explained by its message chain.
53
+ const defect = Cause.findDefect(cause);
54
+ const stack =
55
+ Result.isSuccess(defect) && defect.success instanceof Error ? defect.success.stack : undefined;
56
+
57
+ if (errorFormat === "json") {
58
+ const encode = stack === undefined ? encodeError : encodeErrorWithStack;
59
+
60
+ return Console.error(formatStructured({ error: encode(error) }, "json"));
61
+ }
62
+
63
+ // Toon has no multi-line string, so the stack only reads well under the block.
64
+ const block = formatStructured({ error: encodeError(error) }, "default");
65
+
66
+ return Console.error(stack === undefined ? block : `${block}\n\n${stack}`);
67
+ };
@@ -1,35 +1,21 @@
1
- import { Flag } from "effect/unstable/cli";
1
+ import { Config } from "effect";
2
+ import { Flag, GlobalFlag } from "effect/cli";
2
3
 
3
- /**
4
- * Common server connection options
5
- */
6
- export const hostOption = Flag.string("host").pipe(
7
- Flag.withDescription("Server host"),
8
- Flag.withDefault("127.0.0.1"),
9
- );
10
-
11
- export const portOption = Flag.integer("port").pipe(
12
- Flag.withDescription("Server port"),
13
- Flag.withAlias("p"),
14
- Flag.withDefault(7777),
15
- );
4
+ export const WingsUri = GlobalFlag.Setting("uri")({
5
+ flag: Flag.String("uri").pipe(
6
+ Flag.withDefault("http://localhost:7777"),
7
+ Flag.withDescription("Server URI"),
8
+ ),
9
+ });
16
10
 
17
- /**
18
- * Common pagination options
19
- */
20
- export const pageSizeOption = Flag.integer("page-size").pipe(
21
- Flag.withDescription("Number of items to return (max: 1000)"),
22
- Flag.withDefault(100),
23
- );
24
-
25
- export const pageTokenOption = Flag.optional(
26
- Flag.string("page-token").pipe(Flag.withDescription("Continuation token for pagination")),
27
- );
11
+ export const Output = GlobalFlag.Setting("output")({
12
+ flag: Flag.Literals("output", ["default", "json"] as const).pipe(
13
+ Flag.withDefault("default"),
14
+ Flag.withDescription("Log output format"),
15
+ ),
16
+ });
28
17
 
29
- /**
30
- * Force flag for destructive operations
31
- */
32
- export const forceOption = Flag.boolean("force").pipe(
33
- Flag.withDescription("Skip confirmation prompt"),
34
- Flag.withDefault(false),
18
+ export const catalogFlag = Flag.String("catalog").pipe(
19
+ Flag.withDescription("Catalog ID. Alternatively, use the AIRFOIL_CATALOG environment variable"),
20
+ Flag.withFallbackConfig(Config.NonEmptyString("AIRFOIL_CATALOG")),
35
21
  );