587 lines
17 KiB
JavaScript
587 lines
17 KiB
JavaScript
// 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"]);
|
|
}
|
|
|
|
export { IcebergError, IcebergRestCatalog, getCurrentSchema, isDecimalType, isFixedType, parseDecimalType, parseFixedType, typesEqual };
|
|
//# sourceMappingURL=index.mjs.map
|
|
//# sourceMappingURL=index.mjs.map
|