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
|