Init
This commit is contained in:
+596
@@ -0,0 +1,596 @@
|
||||
'use strict';
|
||||
|
||||
// src/errors/IcebergError.ts
|
||||
var IcebergError = class extends Error {
|
||||
constructor(message, opts) {
|
||||
super(message);
|
||||
this.name = "IcebergError";
|
||||
this.status = opts.status;
|
||||
this.icebergType = opts.icebergType;
|
||||
this.icebergCode = opts.icebergCode;
|
||||
this.details = opts.details;
|
||||
this.isCommitStateUnknown = opts.icebergType === "CommitStateUnknownException" || [500, 502, 504].includes(opts.status) && opts.icebergType?.includes("CommitState") === true;
|
||||
}
|
||||
/**
|
||||
* Returns true if the error is a 404 Not Found error.
|
||||
*/
|
||||
isNotFound() {
|
||||
return this.status === 404;
|
||||
}
|
||||
/**
|
||||
* Returns true if the error is a 409 Conflict error.
|
||||
*/
|
||||
isConflict() {
|
||||
return this.status === 409;
|
||||
}
|
||||
/**
|
||||
* Returns true if the error is a 419 Authentication Timeout error.
|
||||
*/
|
||||
isAuthenticationTimeout() {
|
||||
return this.status === 419;
|
||||
}
|
||||
};
|
||||
|
||||
// src/utils/url.ts
|
||||
function buildUrl(baseUrl, path, query) {
|
||||
const url = new URL(path, baseUrl);
|
||||
if (query) {
|
||||
for (const [key, value] of Object.entries(query)) {
|
||||
if (value !== void 0) {
|
||||
url.searchParams.set(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
// src/http/createFetchClient.ts
|
||||
async function buildAuthHeaders(auth) {
|
||||
if (!auth || auth.type === "none") {
|
||||
return {};
|
||||
}
|
||||
if (auth.type === "bearer") {
|
||||
return { Authorization: `Bearer ${auth.token}` };
|
||||
}
|
||||
if (auth.type === "header") {
|
||||
return { [auth.name]: auth.value };
|
||||
}
|
||||
if (auth.type === "custom") {
|
||||
return await auth.getHeaders();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
function createFetchClient(options) {
|
||||
const fetchFn = options.fetchImpl ?? globalThis.fetch;
|
||||
return {
|
||||
async request({
|
||||
method,
|
||||
path,
|
||||
query,
|
||||
body,
|
||||
headers
|
||||
}) {
|
||||
const url = buildUrl(options.baseUrl, path, query);
|
||||
const authHeaders = await buildAuthHeaders(options.auth);
|
||||
const res = await fetchFn(url, {
|
||||
method,
|
||||
headers: {
|
||||
...body ? { "Content-Type": "application/json" } : {},
|
||||
...authHeaders,
|
||||
...headers
|
||||
},
|
||||
body: body ? JSON.stringify(body) : void 0
|
||||
});
|
||||
const text = await res.text();
|
||||
const isJson = (res.headers.get("content-type") || "").includes("application/json");
|
||||
const data = isJson && text ? JSON.parse(text) : text;
|
||||
if (!res.ok) {
|
||||
const errBody = isJson ? data : void 0;
|
||||
const errorDetail = errBody?.error;
|
||||
throw new IcebergError(
|
||||
errorDetail?.message ?? `Request failed with status ${res.status}`,
|
||||
{
|
||||
status: res.status,
|
||||
icebergType: errorDetail?.type,
|
||||
icebergCode: errorDetail?.code,
|
||||
details: errBody
|
||||
}
|
||||
);
|
||||
}
|
||||
return { status: res.status, headers: res.headers, data };
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// src/catalog/namespaces.ts
|
||||
function namespaceToPath(namespace) {
|
||||
return namespace.join("");
|
||||
}
|
||||
var NamespaceOperations = class {
|
||||
constructor(client, prefix = "") {
|
||||
this.client = client;
|
||||
this.prefix = prefix;
|
||||
}
|
||||
async listNamespaces(parent) {
|
||||
const query = parent ? { parent: namespaceToPath(parent.namespace) } : void 0;
|
||||
const response = await this.client.request({
|
||||
method: "GET",
|
||||
path: `${this.prefix}/namespaces`,
|
||||
query
|
||||
});
|
||||
return response.data.namespaces.map((ns) => ({ namespace: ns }));
|
||||
}
|
||||
async createNamespace(id, metadata) {
|
||||
const request = {
|
||||
namespace: id.namespace,
|
||||
properties: metadata?.properties
|
||||
};
|
||||
const response = await this.client.request({
|
||||
method: "POST",
|
||||
path: `${this.prefix}/namespaces`,
|
||||
body: request
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
async dropNamespace(id) {
|
||||
await this.client.request({
|
||||
method: "DELETE",
|
||||
path: `${this.prefix}/namespaces/${namespaceToPath(id.namespace)}`
|
||||
});
|
||||
}
|
||||
async loadNamespaceMetadata(id) {
|
||||
const response = await this.client.request({
|
||||
method: "GET",
|
||||
path: `${this.prefix}/namespaces/${namespaceToPath(id.namespace)}`
|
||||
});
|
||||
return {
|
||||
properties: response.data.properties
|
||||
};
|
||||
}
|
||||
async namespaceExists(id) {
|
||||
try {
|
||||
await this.client.request({
|
||||
method: "HEAD",
|
||||
path: `${this.prefix}/namespaces/${namespaceToPath(id.namespace)}`
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error instanceof IcebergError && error.status === 404) {
|
||||
return false;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
async createNamespaceIfNotExists(id, metadata) {
|
||||
try {
|
||||
return await this.createNamespace(id, metadata);
|
||||
} catch (error) {
|
||||
if (error instanceof IcebergError && error.status === 409) {
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// src/catalog/tables.ts
|
||||
function namespaceToPath2(namespace) {
|
||||
return namespace.join("");
|
||||
}
|
||||
var TableOperations = class {
|
||||
constructor(client, prefix = "", accessDelegation) {
|
||||
this.client = client;
|
||||
this.prefix = prefix;
|
||||
this.accessDelegation = accessDelegation;
|
||||
}
|
||||
async listTables(namespace) {
|
||||
const response = await this.client.request({
|
||||
method: "GET",
|
||||
path: `${this.prefix}/namespaces/${namespaceToPath2(namespace.namespace)}/tables`
|
||||
});
|
||||
return response.data.identifiers;
|
||||
}
|
||||
async createTable(namespace, request) {
|
||||
const headers = {};
|
||||
if (this.accessDelegation) {
|
||||
headers["X-Iceberg-Access-Delegation"] = this.accessDelegation;
|
||||
}
|
||||
const response = await this.client.request({
|
||||
method: "POST",
|
||||
path: `${this.prefix}/namespaces/${namespaceToPath2(namespace.namespace)}/tables`,
|
||||
body: request,
|
||||
headers
|
||||
});
|
||||
return response.data.metadata;
|
||||
}
|
||||
async updateTable(id, request) {
|
||||
const response = await this.client.request({
|
||||
method: "POST",
|
||||
path: `${this.prefix}/namespaces/${namespaceToPath2(id.namespace)}/tables/${id.name}`,
|
||||
body: request
|
||||
});
|
||||
return {
|
||||
"metadata-location": response.data["metadata-location"],
|
||||
metadata: response.data.metadata
|
||||
};
|
||||
}
|
||||
async dropTable(id, options) {
|
||||
await this.client.request({
|
||||
method: "DELETE",
|
||||
path: `${this.prefix}/namespaces/${namespaceToPath2(id.namespace)}/tables/${id.name}`,
|
||||
query: { purgeRequested: String(options?.purge ?? false) }
|
||||
});
|
||||
}
|
||||
async loadTable(id) {
|
||||
const headers = {};
|
||||
if (this.accessDelegation) {
|
||||
headers["X-Iceberg-Access-Delegation"] = this.accessDelegation;
|
||||
}
|
||||
const response = await this.client.request({
|
||||
method: "GET",
|
||||
path: `${this.prefix}/namespaces/${namespaceToPath2(id.namespace)}/tables/${id.name}`,
|
||||
headers
|
||||
});
|
||||
return response.data.metadata;
|
||||
}
|
||||
async tableExists(id) {
|
||||
const headers = {};
|
||||
if (this.accessDelegation) {
|
||||
headers["X-Iceberg-Access-Delegation"] = this.accessDelegation;
|
||||
}
|
||||
try {
|
||||
await this.client.request({
|
||||
method: "HEAD",
|
||||
path: `${this.prefix}/namespaces/${namespaceToPath2(id.namespace)}/tables/${id.name}`,
|
||||
headers
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error instanceof IcebergError && error.status === 404) {
|
||||
return false;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
async createTableIfNotExists(namespace, request) {
|
||||
try {
|
||||
return await this.createTable(namespace, request);
|
||||
} catch (error) {
|
||||
if (error instanceof IcebergError && error.status === 409) {
|
||||
return await this.loadTable({ namespace: namespace.namespace, name: request.name });
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// src/catalog/IcebergRestCatalog.ts
|
||||
var IcebergRestCatalog = class {
|
||||
/**
|
||||
* Creates a new Iceberg REST Catalog client.
|
||||
*
|
||||
* @param options - Configuration options for the catalog client
|
||||
*/
|
||||
constructor(options) {
|
||||
let prefix = "v1";
|
||||
if (options.catalogName) {
|
||||
prefix += `/${options.catalogName}`;
|
||||
}
|
||||
const baseUrl = options.baseUrl.endsWith("/") ? options.baseUrl : `${options.baseUrl}/`;
|
||||
this.client = createFetchClient({
|
||||
baseUrl,
|
||||
auth: options.auth,
|
||||
fetchImpl: options.fetch
|
||||
});
|
||||
this.accessDelegation = options.accessDelegation?.join(",");
|
||||
this.namespaceOps = new NamespaceOperations(this.client, prefix);
|
||||
this.tableOps = new TableOperations(this.client, prefix, this.accessDelegation);
|
||||
}
|
||||
/**
|
||||
* Lists all namespaces in the catalog.
|
||||
*
|
||||
* @param parent - Optional parent namespace to list children under
|
||||
* @returns Array of namespace identifiers
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // List all top-level namespaces
|
||||
* const namespaces = await catalog.listNamespaces();
|
||||
*
|
||||
* // List namespaces under a parent
|
||||
* const children = await catalog.listNamespaces({ namespace: ['analytics'] });
|
||||
* ```
|
||||
*/
|
||||
async listNamespaces(parent) {
|
||||
return this.namespaceOps.listNamespaces(parent);
|
||||
}
|
||||
/**
|
||||
* Creates a new namespace in the catalog.
|
||||
*
|
||||
* @param id - Namespace identifier to create
|
||||
* @param metadata - Optional metadata properties for the namespace
|
||||
* @returns Response containing the created namespace and its properties
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const response = await catalog.createNamespace(
|
||||
* { namespace: ['analytics'] },
|
||||
* { properties: { owner: 'data-team' } }
|
||||
* );
|
||||
* console.log(response.namespace); // ['analytics']
|
||||
* console.log(response.properties); // { owner: 'data-team', ... }
|
||||
* ```
|
||||
*/
|
||||
async createNamespace(id, metadata) {
|
||||
return this.namespaceOps.createNamespace(id, metadata);
|
||||
}
|
||||
/**
|
||||
* Drops a namespace from the catalog.
|
||||
*
|
||||
* The namespace must be empty (contain no tables) before it can be dropped.
|
||||
*
|
||||
* @param id - Namespace identifier to drop
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await catalog.dropNamespace({ namespace: ['analytics'] });
|
||||
* ```
|
||||
*/
|
||||
async dropNamespace(id) {
|
||||
await this.namespaceOps.dropNamespace(id);
|
||||
}
|
||||
/**
|
||||
* Loads metadata for a namespace.
|
||||
*
|
||||
* @param id - Namespace identifier to load
|
||||
* @returns Namespace metadata including properties
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const metadata = await catalog.loadNamespaceMetadata({ namespace: ['analytics'] });
|
||||
* console.log(metadata.properties);
|
||||
* ```
|
||||
*/
|
||||
async loadNamespaceMetadata(id) {
|
||||
return this.namespaceOps.loadNamespaceMetadata(id);
|
||||
}
|
||||
/**
|
||||
* Lists all tables in a namespace.
|
||||
*
|
||||
* @param namespace - Namespace identifier to list tables from
|
||||
* @returns Array of table identifiers
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const tables = await catalog.listTables({ namespace: ['analytics'] });
|
||||
* console.log(tables); // [{ namespace: ['analytics'], name: 'events' }, ...]
|
||||
* ```
|
||||
*/
|
||||
async listTables(namespace) {
|
||||
return this.tableOps.listTables(namespace);
|
||||
}
|
||||
/**
|
||||
* Creates a new table in the catalog.
|
||||
*
|
||||
* @param namespace - Namespace to create the table in
|
||||
* @param request - Table creation request including name, schema, partition spec, etc.
|
||||
* @returns Table metadata for the created table
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const metadata = await catalog.createTable(
|
||||
* { namespace: ['analytics'] },
|
||||
* {
|
||||
* name: 'events',
|
||||
* schema: {
|
||||
* type: 'struct',
|
||||
* fields: [
|
||||
* { id: 1, name: 'id', type: 'long', required: true },
|
||||
* { id: 2, name: 'timestamp', type: 'timestamp', required: true }
|
||||
* ],
|
||||
* 'schema-id': 0
|
||||
* },
|
||||
* 'partition-spec': {
|
||||
* 'spec-id': 0,
|
||||
* fields: [
|
||||
* { source_id: 2, field_id: 1000, name: 'ts_day', transform: 'day' }
|
||||
* ]
|
||||
* }
|
||||
* }
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
async createTable(namespace, request) {
|
||||
return this.tableOps.createTable(namespace, request);
|
||||
}
|
||||
/**
|
||||
* Updates an existing table's metadata.
|
||||
*
|
||||
* Can update the schema, partition spec, or properties of a table.
|
||||
*
|
||||
* @param id - Table identifier to update
|
||||
* @param request - Update request with fields to modify
|
||||
* @returns Response containing the metadata location and updated table metadata
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const response = await catalog.updateTable(
|
||||
* { namespace: ['analytics'], name: 'events' },
|
||||
* {
|
||||
* properties: { 'read.split.target-size': '134217728' }
|
||||
* }
|
||||
* );
|
||||
* console.log(response['metadata-location']); // s3://...
|
||||
* console.log(response.metadata); // TableMetadata object
|
||||
* ```
|
||||
*/
|
||||
async updateTable(id, request) {
|
||||
return this.tableOps.updateTable(id, request);
|
||||
}
|
||||
/**
|
||||
* Drops a table from the catalog.
|
||||
*
|
||||
* @param id - Table identifier to drop
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await catalog.dropTable({ namespace: ['analytics'], name: 'events' });
|
||||
* ```
|
||||
*/
|
||||
async dropTable(id, options) {
|
||||
await this.tableOps.dropTable(id, options);
|
||||
}
|
||||
/**
|
||||
* Loads metadata for a table.
|
||||
*
|
||||
* @param id - Table identifier to load
|
||||
* @returns Table metadata including schema, partition spec, location, etc.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const metadata = await catalog.loadTable({ namespace: ['analytics'], name: 'events' });
|
||||
* console.log(metadata.schema);
|
||||
* console.log(metadata.location);
|
||||
* ```
|
||||
*/
|
||||
async loadTable(id) {
|
||||
return this.tableOps.loadTable(id);
|
||||
}
|
||||
/**
|
||||
* Checks if a namespace exists in the catalog.
|
||||
*
|
||||
* @param id - Namespace identifier to check
|
||||
* @returns True if the namespace exists, false otherwise
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const exists = await catalog.namespaceExists({ namespace: ['analytics'] });
|
||||
* console.log(exists); // true or false
|
||||
* ```
|
||||
*/
|
||||
async namespaceExists(id) {
|
||||
return this.namespaceOps.namespaceExists(id);
|
||||
}
|
||||
/**
|
||||
* Checks if a table exists in the catalog.
|
||||
*
|
||||
* @param id - Table identifier to check
|
||||
* @returns True if the table exists, false otherwise
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const exists = await catalog.tableExists({ namespace: ['analytics'], name: 'events' });
|
||||
* console.log(exists); // true or false
|
||||
* ```
|
||||
*/
|
||||
async tableExists(id) {
|
||||
return this.tableOps.tableExists(id);
|
||||
}
|
||||
/**
|
||||
* Creates a namespace if it does not exist.
|
||||
*
|
||||
* If the namespace already exists, returns void. If created, returns the response.
|
||||
*
|
||||
* @param id - Namespace identifier to create
|
||||
* @param metadata - Optional metadata properties for the namespace
|
||||
* @returns Response containing the created namespace and its properties, or void if it already exists
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const response = await catalog.createNamespaceIfNotExists(
|
||||
* { namespace: ['analytics'] },
|
||||
* { properties: { owner: 'data-team' } }
|
||||
* );
|
||||
* if (response) {
|
||||
* console.log('Created:', response.namespace);
|
||||
* } else {
|
||||
* console.log('Already exists');
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
async createNamespaceIfNotExists(id, metadata) {
|
||||
return this.namespaceOps.createNamespaceIfNotExists(id, metadata);
|
||||
}
|
||||
/**
|
||||
* Creates a table if it does not exist.
|
||||
*
|
||||
* If the table already exists, returns its metadata instead.
|
||||
*
|
||||
* @param namespace - Namespace to create the table in
|
||||
* @param request - Table creation request including name, schema, partition spec, etc.
|
||||
* @returns Table metadata for the created or existing table
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const metadata = await catalog.createTableIfNotExists(
|
||||
* { namespace: ['analytics'] },
|
||||
* {
|
||||
* name: 'events',
|
||||
* schema: {
|
||||
* type: 'struct',
|
||||
* fields: [
|
||||
* { id: 1, name: 'id', type: 'long', required: true },
|
||||
* { id: 2, name: 'timestamp', type: 'timestamp', required: true }
|
||||
* ],
|
||||
* 'schema-id': 0
|
||||
* }
|
||||
* }
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
async createTableIfNotExists(namespace, request) {
|
||||
return this.tableOps.createTableIfNotExists(namespace, request);
|
||||
}
|
||||
};
|
||||
|
||||
// src/catalog/types.ts
|
||||
var DECIMAL_REGEX = /^decimal\s*\(\s*(\d+)\s*,\s*(\d+)\s*\)$/;
|
||||
var FIXED_REGEX = /^fixed\s*\[\s*(\d+)\s*\]$/;
|
||||
function parseDecimalType(type) {
|
||||
const match = type.match(DECIMAL_REGEX);
|
||||
if (!match) return null;
|
||||
return {
|
||||
precision: parseInt(match[1], 10),
|
||||
scale: parseInt(match[2], 10)
|
||||
};
|
||||
}
|
||||
function parseFixedType(type) {
|
||||
const match = type.match(FIXED_REGEX);
|
||||
if (!match) return null;
|
||||
return {
|
||||
length: parseInt(match[1], 10)
|
||||
};
|
||||
}
|
||||
function isDecimalType(type) {
|
||||
return DECIMAL_REGEX.test(type);
|
||||
}
|
||||
function isFixedType(type) {
|
||||
return FIXED_REGEX.test(type);
|
||||
}
|
||||
function typesEqual(a, b) {
|
||||
const decimalA = parseDecimalType(a);
|
||||
const decimalB = parseDecimalType(b);
|
||||
if (decimalA && decimalB) {
|
||||
return decimalA.precision === decimalB.precision && decimalA.scale === decimalB.scale;
|
||||
}
|
||||
const fixedA = parseFixedType(a);
|
||||
const fixedB = parseFixedType(b);
|
||||
if (fixedA && fixedB) {
|
||||
return fixedA.length === fixedB.length;
|
||||
}
|
||||
return a === b;
|
||||
}
|
||||
function getCurrentSchema(metadata) {
|
||||
return metadata.schemas.find((s) => s["schema-id"] === metadata["current-schema-id"]);
|
||||
}
|
||||
|
||||
exports.IcebergError = IcebergError;
|
||||
exports.IcebergRestCatalog = IcebergRestCatalog;
|
||||
exports.getCurrentSchema = getCurrentSchema;
|
||||
exports.isDecimalType = isDecimalType;
|
||||
exports.isFixedType = isFixedType;
|
||||
exports.parseDecimalType = parseDecimalType;
|
||||
exports.parseFixedType = parseFixedType;
|
||||
exports.typesEqual = typesEqual;
|
||||
//# sourceMappingURL=index.cjs.map
|
||||
//# sourceMappingURL=index.cjs.map
|
||||
Reference in New Issue
Block a user