@tonytang99/integration-canonical
v1.2.0
Published
Canonical data models for integration platform
Maintainers
Readme
@tonytang99/integration-canonical
Canonical data models for the integration platform. These schemas provide a standardized intermediate format for data flowing between different e-commerce and ERP systems.
Installation
npm install @tonytang99/integration-canonicalPhilosophy
The canonical schema is the common language spoken between all systems. Instead of building direct integrations between every pair of systems (N×N complexity), we transform data to/from canonical format (2N complexity).
BigCommerce → Canonical → MYOB
Adobe Commerce → Canonical → NetSuite
Shopify → Canonical → XeroCommon Types
Money
Money is represented in minor units (cents) to avoid floating-point precision issues.
import { MoneySchema, MoneyHelper, type Money } from '@tonytang99/integration-canonical';
// Create money from dollars
const price = MoneyHelper.fromMajor(19.99, 'USD');
// { amount: 1999, currency: 'USD' }
// Convert to dollars
MoneyHelper.toMajor(price); // 19.99
// Format for display
MoneyHelper.format(price, 'en-US'); // "$19.99"
// Math operations
const total = MoneyHelper.add(
{ amount: 1999, currency: 'USD' },
{ amount: 500, currency: 'USD' }
); // { amount: 2499, currency: 'USD' }
const discounted = MoneyHelper.multiply(
{ amount: 1999, currency: 'USD' },
0.9
); // 10% offWhy Minor Units?
// ❌ BAD: Floating point errors
0.1 + 0.2 // 0.30000000000000004
// ✅ GOOD: Integer math
10 + 20 // 30 (representing $0.30)Address
Comprehensive address schema supporting international addresses.
import { AddressSchema, AddressHelper, type Address } from '@tonytang99/integration-canonical';
const address: Address = {
street1: '123 Main Street',
street2: 'Suite 100',
city: 'San Francisco',
state: 'California',
stateCode: 'CA',
postalCode: '94102',
country: 'United States',
countryCode: 'US',
// Optional contact info
firstName: 'John',
lastName: 'Doe',
company: 'Acme Corp',
phone: '555-1234',
email: '[email protected]',
// Metadata
type: 'billing',
isDefault: true,
};
// Format address
AddressHelper.format(address);
// Output:
// Acme Corp
// John Doe
// 123 Main Street
// Suite 100
// San Francisco, CA, 94102
// United States
// Single line
AddressHelper.formatSingleLine(address);
// "Acme Corp, John Doe, 123 Main Street, Suite 100, San Francisco, CA, 94102, United States"
// Validate postal code
AddressHelper.isValidPostalCode('94102', 'US'); // true
AddressHelper.isValidPostalCode('941', 'US'); // falsePhone Number
import { PhoneNumberSchema, PhoneNumberHelper } from '@tonytang99/integration-canonical';
const phone = {
number: '5551234567',
countryCode: '+1',
extension: '123',
type: 'mobile',
};
PhoneNumberHelper.format(phone); // "+1 5551234567 ext. 123"Product Schema
Normalized product representation supporting:
- Simple products
- Products with variants (size, color, etc.)
- Pricing with compare-at prices (sales)
- Inventory tracking
- Images and media
- Physical properties (weight, dimensions)
- SEO metadata
import {
CanonicalProductSchema,
ProductHelper,
MoneyHelper,
type CanonicalProduct
} from '@tonytang99/integration-canonical';
const product: CanonicalProduct = {
id: 'prod_123',
sku: 'WGT-ABC-001',
name: 'Premium Widget',
description: 'A high-quality widget for all your needs',
price: MoneyHelper.fromMajor(29.99, 'USD'),
compareAtPrice: MoneyHelper.fromMajor(39.99, 'USD'), // "Was $39.99"
inventory: {
quantity: 100,
tracked: true,
allowBackorder: false,
lowStockLevel: 10,
},
weight: {
value: 2.5,
unit: 'kg',
},
dimensions: {
length: 30,
width: 20,
height: 10,
unit: 'cm',
},
categories: ['widgets', 'premium'],
brand: 'Acme',
images: [
{
url: 'https://cdn.example.com/widget-1.jpg',
altText: 'Premium Widget Front View',
sortOrder: 1,
isThumbnail: true,
},
],
isVisible: true,
availability: 'available',
condition: 'new',
metadata: {
source: 'bigcommerce',
sourceId: '12345',
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-30T10:00:00Z',
},
};
// Validate
CanonicalProductSchema.parse(product); // throws if invalid
// Helper functions
ProductHelper.isInStock(product); // true
ProductHelper.isLowStock(product); // false (100 > 10)
const displayPrice = ProductHelper.getDisplayPrice(product);
// {
// price: { amount: 2999, currency: 'USD' },
// isOnSale: true,
// discount: { amount: 1000, currency: 'USD' }
// }
const thumbnail = ProductHelper.getThumbnail(product);
// { url: '...', altText: '...', isThumbnail: true }
const weightInKg = ProductHelper.getWeightInKg(product); // 2.5Product Variants
For products with options (size, color, etc.):
const tshirt: CanonicalProduct = {
id: 'prod_tshirt',
sku: 'TSHIRT-BASE',
name: 'Premium T-Shirt',
price: MoneyHelper.fromMajor(24.99, 'USD'),
hasVariants: true,
variants: [
{
id: 'var_1',
sku: 'TSHIRT-RED-M',
options: {
color: 'Red',
size: 'Medium',
},
inventory: {
quantity: 50,
tracked: true,
},
},
{
id: 'var_2',
sku: 'TSHIRT-BLU-L',
options: {
color: 'Blue',
size: 'Large',
},
inventory: {
quantity: 30,
tracked: true,
},
},
],
inventory: {
quantity: 80, // Total across all variants
tracked: true,
},
metadata: {
source: 'bigcommerce',
sourceId: '67890',
},
};
// Get specific variant
const variant = ProductHelper.getVariantBySku(tshirt, 'TSHIRT-RED-M');
// Get total inventory
const total = ProductHelper.getTotalInventory(tshirt); // 80Design Principles
1. Explicit Over Implicit
All fields are explicitly typed. No any types unless absolutely necessary.
// ✅ Good
price: MoneySchema
// ❌ Bad
price: z.any()2. Required vs Optional
Core fields are required. System-specific fields are optional.
{
sku: z.string(), // Required - every product has a SKU
name: z.string(), // Required - every product has a name
brand: z.string().optional(), // Optional - not all systems track brand
}3. Metadata for System-Specific Data
Use metadata and customFields for data that doesn't fit the core schema.
{
metadata: {
source: 'bigcommerce',
sourceId: '12345',
externalIds: {
myob: 'STOCK-ABC',
netsuite: 'ITEM-999',
},
},
customFields: {
bigcommerce_product_type: 'physical',
myob_default_location: 'WAREHOUSE-A',
},
}4. Validation at Boundaries
Validate when data enters or leaves the canonical format.
// ✅ Validate when transforming TO canonical
const canonical = CanonicalProductSchema.parse(transformed);
// ✅ Validate when transforming FROM canonical
const myobData = MyobStockItemSchema.parse(transformed);Common Patterns
Transforming TO Canonical
import { CanonicalProductSchema, MoneyHelper } from '@tonytang99/integration-canonical';
function bigCommerceToCanonical(bcProduct: any): CanonicalProduct {
const canonical = {
id: String(bcProduct.id),
sku: bcProduct.sku,
name: bcProduct.name,
price: MoneyHelper.fromMajor(bcProduct.price, 'USD'),
inventory: {
quantity: bcProduct.inventory_level,
tracked: bcProduct.inventory_tracking !== 'none',
},
metadata: {
source: 'bigcommerce',
sourceId: String(bcProduct.id),
},
};
// Validate before returning
return CanonicalProductSchema.parse(canonical);
}Transforming FROM Canonical
function canonicalToMyob(product: CanonicalProduct) {
return {
InventoryID: product.sku,
Description: product.name,
BasePrice: MoneyHelper.toMajor(product.price),
QuantityOnHand: product.inventory.quantity,
// ... more MYOB fields
};
}Testing
import { CanonicalProductSchema, MoneyHelper } from '@tonytang99/integration-canonical';
describe('Product transformation', () => {
it('should transform BC product to canonical', () => {
const bcProduct = {
id: 123,
sku: 'ABC',
name: 'Widget',
price: 19.99,
inventory_level: 50,
};
const canonical = {
id: '123',
sku: 'ABC',
name: 'Widget',
price: MoneyHelper.fromMajor(19.99, 'USD'),
inventory: {
quantity: 50,
tracked: true,
},
metadata: {
source: 'bigcommerce',
sourceId: '123',
},
};
// This will throw if schema is invalid
expect(() => CanonicalProductSchema.parse(canonical)).not.toThrow();
});
});Future Schemas
Coming soon:
CanonicalOrder- Orders and line itemsCanonicalCustomer- Customer dataCanonicalInventory- Inventory movementsCanonicalCategory- Product categoriesCanonicalPayment- Payment transactionsCanonicalShipment- Shipping information
License
MIT
