Skip to Content
Jumentix DocsPackages@jumentix/canaUsage guideSchema and keys

Schema and keys

Cana schema is plain data. IndexedDB applies schema changes only when the database version increases, so versioning is part of your application contract.

Versioned schema

import { createClient, validateSchema, type CanaSchema } from '@jumentix/cana';

const schemaV1: CanaSchema = {
  version: 1,
  stores: [
    {
      name: 'categories',
      keyPath: 'id',
      indexes: [{ name: 'byName', keyPath: 'name', unique: true }]
    }
  ]
};

const schemaV2: CanaSchema = {
  version: 2,
  stores: [
    {
      name: 'categories',
      keyPath: 'id',
      indexes: [{ name: 'byName', keyPath: 'name', unique: true }]
    },
    {
      name: 'tasks',
      keyPath: 'id',
      indexes: [
        { name: 'byCategory', keyPath: 'categoryId' },
        { name: 'byUpdatedAt', keyPath: 'updatedAt' }
      ]
    }
  ]
};

const problems = validateSchema(schemaV2);
if (problems.length > 0) {
  throw new Error(problems.join('\n'));
}

const firstClient = createClient({ name: 'tasks-schema-demo', schema: schemaV1 });
await firstClient.open();
await firstClient.table('categories').put({ id: 'work', name: 'Work' });
await firstClient.close();

const upgradedClient = createClient({ name: 'tasks-schema-demo', schema: schemaV2 });
await upgradedClient.open();
await upgradedClient.table('tasks').put({
  id: 'task-1',
  title: 'Created after schema upgrade',
  categoryId: 'work',
  completed: false,
  priority: 'medium',
  createdAt: Date.now(),
  updatedAt: Date.now()
});

Upgrade rules

ChangeCana behavior
Add storeApplied during the next higher version.
Add indexApplied during the next higher version.
Remove a store from the schema objectThe old store remains. Cana avoids accidental data loss.
Change an existing index definitionThe old index remains. Use a new index name to rebuild intentionally.
Open an older version over newer dataRefused with UpgradeFailed.

Key strategies

const inboundSchema: CanaSchema = {
  version: 1,
  stores: [{ name: 'tasks', keyPath: 'id' }]
};

const generatedInboundSchema: CanaSchema = {
  version: 1,
  stores: [{ name: 'tasks', keyPath: 'id', autoIncrement: true }]
};

const outboundSchema: CanaSchema = {
  version: 1,
  stores: [{ name: 'cache' }]
};

Use inbound keys for application records such as categories and tasks. Use outbound keys for cache entries where the key is metadata outside the stored object.

await client.table('tasks').put({
  id: 'task-1',
  title: 'Stable inbound key',
  categoryId: 'work',
  completed: false,
  priority: 'medium',
  createdAt: 1,
  updatedAt: 1
});

await client.table('cache').put(
  { body: '<html>cached response</html>', savedAt: Date.now() },
  'https://example.com/tasks'
);

Run it here

Schema upgrade

Start with Category, then raise the version and add the Task table.

const v1 = cana.createClient({
  name: dbName,
  schema: {
    version: 1,
    stores: [{ name: 'categories', keyPath: 'id' }]
  }
});
await v1.open();
await v1.table('categories').add({ id: 'work', name: 'Work' });
await v1.close();

const v2 = cana.createClient({
  name: dbName,
  schema: {
  version: 1,
  stores: [
    { name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
    {
      name: 'tasks',
      keyPath: 'id',
      indexes: [
        { name: 'byCategory', keyPath: 'categoryId' },
        { name: 'byCompleted', keyPath: 'completed' },
        { name: 'byUpdatedAt', keyPath: 'updatedAt' }
      ]
    }
  ]
}
});
await v2.open();
await v2.table('tasks').add({
  id: 'task-1',
  title: 'Created after upgrade',
  categoryId: 'work',
  completed: false,
  priority: 'medium',
  createdAt: Date.now(),
  updatedAt: Date.now()
});
return {
  categories: await v2.table('categories').query(),
  tasks: await v2.table('tasks').query()
};

Keys

Use stable ids in Category and Task records.

const client = cana.createClient({
  name: dbName,
  schema: {
  version: 1,
  stores: [
    { name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
    {
      name: 'tasks',
      keyPath: 'id',
      indexes: [
        { name: 'byCategory', keyPath: 'categoryId' },
        { name: 'byCompleted', keyPath: 'completed' },
        { name: 'byUpdatedAt', keyPath: 'updatedAt' }
      ]
    }
  ]
}
});
await client.open();
await client.table('categories').add({
  id: 'docs',
  name: 'Docs',
  color: '#0f766e',
  createdAt: 1,
  updatedAt: 1
});
await client.table('tasks').add({
  id: 'docs-1',
  title: 'Document stable keys',
  categoryId: 'docs',
  completed: false,
  priority: 'medium',
  createdAt: 2,
  updatedAt: 2
});
return {
  categoryKey: 'docs',
  taskKey: 'docs-1',
  task: await client.table('tasks').get('docs-1')
};

Next

Continue to reading, writing and bulk operations.