JSPM

@icetype/adapters

0.3.0
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 3
  • Score
    100M100P100Q33282F
  • License MIT

IceType adapter abstraction layer for schema transformations

Package Exports

  • @icetype/adapters
  • @icetype/adapters/lazy
  • @icetype/adapters/registry
  • @icetype/adapters/types

Readme

@icetype/adapters

Adapter abstraction layer for IceType schema transformations. This package provides a unified interface for transforming IceType schemas to various output formats like Apache Iceberg, Parquet, SQL, and more.

Installation

npm install @icetype/adapters
# or
pnpm add @icetype/adapters

Usage

import { parseSchema } from '@icetype/core';
import { createAdapterRegistry, globalRegistry } from '@icetype/adapters';
import { IcebergAdapter, ParquetAdapter } from '@icetype/iceberg';

// Parse an IceType schema
const schema = parseSchema({
  $type: 'User',
  id: 'uuid!',
  email: 'string#',
  name: 'string',
});

// Create a registry and register adapters
const registry = createAdapterRegistry();
registry.register(new IcebergAdapter());
registry.register(new ParquetAdapter());

// Use the Iceberg adapter
const icebergAdapter = registry.get('iceberg');
const metadata = icebergAdapter?.transform(schema, {
  location: 's3://my-bucket/tables/users',
});

API

Registry Functions

createAdapterRegistry()

Creates a new, isolated adapter registry instance.

import { createAdapterRegistry } from '@icetype/adapters';

const registry = createAdapterRegistry();
registry.register(myAdapter);

Use this when you need separate adapter configurations for different contexts (testing, multi-tenant apps, etc.).

globalRegistry

A singleton registry for application-wide adapter registration.

import { globalRegistry } from '@icetype/adapters';

// Register at startup
globalRegistry.register(new IcebergAdapter());

// Use anywhere in your app
const adapter = globalRegistry.get('iceberg');

Registry Methods

Method Description Returns
register(adapter) Register an adapter void (throws if duplicate)
get(name) Get adapter by name SchemaAdapter | undefined
has(name) Check if adapter exists boolean
list() List all adapter names string[]
unregister(name) Remove an adapter boolean
clear() Remove all adapters void

Types

SchemaAdapter<TOutput, TOptions>

Interface for implementing custom adapters:

interface SchemaAdapter<TOutput = unknown, TOptions = unknown> {
  readonly name: string;           // Unique adapter identifier
  readonly version: string;        // Semver version
  transform(schema: IceTypeSchema, options?: TOptions): TOutput;
  serialize(output: TOutput): string;
  serializeWithIndexes?(output: TOutput): string;  // Optional
}

IcebergAdapterOptions

Options for the Iceberg adapter:

interface IcebergAdapterOptions {
  location: string;                      // Table location (required)
  tableUuid?: string;                    // Optional table UUID
  properties?: Record<string, string>;   // Iceberg table properties
}

ParquetAdapterOptions

Options for the Parquet adapter:

interface ParquetAdapterOptions {
  format?: 'object' | 'string';  // Output format (default: 'object')
}

Creating Custom Adapters

Implement the SchemaAdapter interface to create your own adapter:

import type { SchemaAdapter, IceTypeSchema } from '@icetype/adapters';

interface JsonSchemaOutput {
  $schema: string;
  type: string;
  properties: Record<string, unknown>;
  required: string[];
}

interface JsonSchemaOptions {
  draft?: '2020-12' | '2019-09' | 'draft-07';
}

const jsonSchemaAdapter: SchemaAdapter<JsonSchemaOutput, JsonSchemaOptions> = {
  name: 'json-schema',
  version: '1.0.0',

  transform(schema: IceTypeSchema, options?: JsonSchemaOptions): JsonSchemaOutput {
    const draft = options?.draft ?? '2020-12';
    return {
      $schema: `https://json-schema.org/draft/${draft}/schema`,
      type: 'object',
      properties: mapFieldsToProperties(schema.fields),
      required: getRequiredFields(schema.fields),
    };
  },

  serialize(output: JsonSchemaOutput): string {
    return JSON.stringify(output, null, 2);
  },
};

Error Handling

The registry throws AdapterError when attempting to register a duplicate adapter:

import { isAdapterError, ErrorCodes } from '@icetype/core';
import { createAdapterRegistry } from '@icetype/adapters';

const registry = createAdapterRegistry();
registry.register(myAdapter);

try {
  registry.register(myAdapter); // Duplicate!
} catch (error) {
  if (isAdapterError(error)) {
    console.log(error.code);        // 'ADAPTER_ALREADY_REGISTERED'
    console.log(error.adapterName); // 'my-adapter'
    console.log(error.operation);   // 'register'
  }
}

Error Codes

Code Description
ADAPTER_ALREADY_REGISTERED Attempted to register an adapter with a name that already exists

Replacing an Adapter

To replace an existing adapter, unregister it first:

registry.unregister('my-adapter');
registry.register(newVersionAdapter);

Testing

When using globalRegistry in tests, clear it between tests to prevent pollution:

import { globalRegistry } from '@icetype/adapters';

describe('MyTests', () => {
  beforeEach(() => {
    globalRegistry.clear();
  });

  it('should work with mock adapter', () => {
    globalRegistry.register(mockAdapter);
    // ... test code
  });
});

For isolated tests, use createAdapterRegistry() instead:

it('should use custom adapter', () => {
  const registry = createAdapterRegistry();
  registry.register(testAdapter);
  // Changes don't affect other tests
});

Adapter Name Conventions

Adapter names can be any string, but we recommend:

  • Use lowercase letters
  • Use hyphens for multi-word names
  • Be descriptive

Good examples:

  • iceberg
  • parquet
  • json-schema
  • sqlite-ddl

Supported but not recommended:

  • Names with special characters
  • Unicode names
  • Empty strings
  • Names with whitespace

Bundle Size Optimization

This package is designed for optimal bundle size through tree-shaking and code splitting.

Tree-Shaking

The package is marked with "sideEffects": false in package.json, enabling bundlers to eliminate unused code. Only the functions you actually import will be included in your bundle.

Separate Entry Points

For minimal bundle size, import only what you need from specific entry points:

// Full package - includes everything
import { createAdapterRegistry, lazyLoadAdapter } from '@icetype/adapters';

// Minimal - just the lazy loading utilities (smallest)
import { lazyLoadAdapter, createLazyAdapterRegistry } from '@icetype/adapters/lazy';

// Registry only - no lazy loading
import { createAdapterRegistry, globalRegistry } from '@icetype/adapters/registry';

// Types only - zero runtime cost
import type { SchemaAdapter, AdapterRegistry } from '@icetype/adapters/types';

Dynamic Adapter Loading

For the smallest possible initial bundle, use dynamic imports with lazy loading:

import { lazyLoadAdapter } from '@icetype/adapters/lazy';

// Adapter code is only loaded when needed
async function generateSQL(schema, dbType: 'postgres' | 'mysql') {
  const adapter = await lazyLoadAdapter(dbType);
  return adapter.transform(schema);
}

Or use the lazy registry for on-demand loading:

import { createLazyAdapterRegistry } from '@icetype/adapters/lazy';

const registry = createLazyAdapterRegistry();

// Register loaders - adapters aren't loaded yet
registry.registerLoader('postgres', async () => {
  const { createPostgresAdapter } = await import('@icetype/postgres');
  return createPostgresAdapter();
});

registry.registerLoader('mysql', async () => {
  const { createMySQLAdapter } = await import('@icetype/mysql');
  return createMySQLAdapter();
});

// Adapter is loaded only when first requested
const adapter = await registry.getAsync('postgres');

Import Recommendations by Use Case

Use Case Recommended Import Bundle Impact
Single adapter, known at build time Direct import from adapter package Minimal
Multiple adapters, selected at runtime @icetype/adapters/lazy Small base + on-demand
Application-wide adapter registry @icetype/adapters/registry Medium
Full functionality @icetype/adapters Full
Type definitions only @icetype/adapters/types Zero runtime

Adapter Package Imports

Each adapter is a separate package for maximum code splitting:

// Only loads postgres-specific code
const { createPostgresAdapter } = await import('@icetype/postgres');

// Only loads mysql-specific code
const { createMySQLAdapter } = await import('@icetype/mysql');

This pattern works well with bundlers like Vite, esbuild, and webpack that support dynamic imports and code splitting.

Examples

Basic Registry Usage

import { createAdapterRegistry } from '@icetype/adapters';
import { PostgresAdapter } from '@icetype/postgres';
import { MySQLAdapter } from '@icetype/mysql';
import { parseSchema } from '@icetype/core';

// Create registry
const registry = createAdapterRegistry();
registry.register(new PostgresAdapter());
registry.register(new MySQLAdapter());

// Parse schema
const schema = parseSchema({
  $type: 'User',
  id: 'uuid!',
  email: 'string#',
  name: 'string',
});

// Generate DDL for PostgreSQL
const pgAdapter = registry.get('postgres');
const pgDDL = pgAdapter?.transform(schema, { schema: 'public' });
const pgSQL = pgAdapter?.serialize(pgDDL);

// Generate DDL for MySQL
const mysqlAdapter = registry.get('mysql');
const mysqlDDL = mysqlAdapter?.transform(schema);
const mysqlSQL = mysqlAdapter?.serialize(mysqlDDL);

Multi-Dialect Support

import { createAdapterRegistry } from '@icetype/adapters';

function generateDDL(schema, dialect: string) {
  const adapter = registry.get(dialect);
  if (!adapter) {
    throw new Error(`Unknown dialect: ${dialect}`);
  }
  const ddl = adapter.transform(schema);
  return adapter.serialize(ddl);
}

// Use with any registered adapter
const pgSQL = generateDDL(userSchema, 'postgres');
const mysqlSQL = generateDDL(userSchema, 'mysql');
const sqliteSQL = generateDDL(userSchema, 'sqlite');

License

MIT