Skip to content

feat: add centralized datasource mapping for package/namespace-level routing#1143

Merged
hotlong merged 1 commit intomainfrom
claude/use-different-data-sources
Apr 14, 2026
Merged

feat: add centralized datasource mapping for package/namespace-level routing#1143
hotlong merged 1 commit intomainfrom
claude/use-different-data-sources

Conversation

@Claude
Copy link
Copy Markdown
Contributor

@Claude Claude AI commented Apr 14, 2026

Implements centralized datasource routing configuration to eliminate per-object datasource field configuration. Enables routing by namespace, package ID, or glob patterns with priority-based resolution.

Changes

Schema Definitions

  • DatasourceMappingRuleSchema in packages/spec/src/stack.zod.ts: Defines routing rules supporting namespace, package, objectPattern (glob), priority, and default fallback
  • datasourceMapping field added to ObjectStackDefinitionSchema: Accepts array of routing rules
  • defaultDatasource field added to ManifestSchema: Package-level default datasource configuration

ObjectQL Engine

  • 4-tier resolution priority in getDriver():
    1. Object's explicit datasource field
    2. datasourceMapping rules (evaluated by priority)
    3. Package's defaultDatasource
    4. Global default driver
  • resolveDatasourceFromMapping(): Evaluates rules with namespace/package/pattern matching
  • matchPattern(): Glob-style pattern matching (*, ? wildcards)
  • setDatasourceMapping(): Public API to configure rules
  • Manifest registry: Stores package manifests for defaultDatasource lookup

Runtime Integration

  • AppPlugin: Automatically calls ql.setDatasourceMapping() when stack contains datasourceMapping

Testing & Documentation

  • Comprehensive test suite covering namespace, pattern, priority, default, and override scenarios
  • Complete usage documentation with examples and migration guide

Example Usage

export default defineStack({
  plugins: [
    new ObjectQLPlugin(),
    new DriverPlugin(new TursoDriver({ url: 'file:./data/system.db' }), 'turso'),
    new DriverPlugin(new InMemoryDriver(), 'memory'),
    new AppPlugin(CrmApp),
  ],
  
  datasourceMapping: [
    // System objects → Turso (persistent)
    { objectPattern: 'sys_*', datasource: 'turso' },
    { namespace: 'auth', datasource: 'turso' },
    
    // CRM → Memory (dev/test)
    { namespace: 'crm', datasource: 'memory' },
    
    // Default → Turso
    { default: true, datasource: 'turso' },
  ],
});

Architecture

Inspired by Django's Database Router and Kubernetes StorageClass patterns. Resolution priority ensures explicit object configuration always wins, with centralized mapping providing sensible defaults for packages/namespaces.

Pattern matching enables batch configuration: sys_* routes all system objects, temp_* routes temporary objects, etc.

Fully backward compatible—existing explicit datasource fields work unchanged.

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 14, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
objectstack-demo Error Error Apr 14, 2026 0:09am
spec Building Building Preview, Comment Apr 14, 2026 0:09am

Request Review

@hotlong hotlong marked this pull request as ready for review April 14, 2026 12:09
Copilot AI review requested due to automatic review settings April 14, 2026 12:09
@github-actions github-actions bot added documentation Improvements or additions to documentation tests size/l labels Apr 14, 2026
@hotlong hotlong merged commit fecea08 into main Apr 14, 2026
10 of 14 checks passed
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a centralized datasource routing mechanism to ObjectStack so stacks can route objects to drivers by namespace/package/pattern, reducing per-object datasource configuration and enabling package-level defaults.

Changes:

  • Introduces datasourceMapping and DatasourceMappingRuleSchema to the stack definition schema.
  • Extends package manifests with defaultDatasource and updates ObjectQL driver resolution to use a 4-tier priority.
  • Adds runtime wiring + tests + standalone documentation for the feature.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 15 comments.

Show a summary per file
File Description
packages/spec/src/stack.zod.ts Adds DatasourceMappingRuleSchema and datasourceMapping to stack schema.
packages/spec/src/kernel/manifest.zod.ts Adds defaultDatasource to manifests for package-level defaults.
packages/objectql/src/engine.ts Implements mapping + manifest-aware driver selection; adds mapping setter and manifest registry.
packages/runtime/src/app-plugin.ts Attempts to configure ObjectQL mapping during app plugin startup.
packages/objectql/src/datasource-mapping.test.ts Adds tests intended to cover namespace/pattern/priority/default/override behavior.
DATASOURCE_MAPPING.md User-facing documentation and migration guidance for datasource mapping.
IMPLEMENTATION_SUMMARY.md High-level implementation recap and rationale.

Comment on lines +665 to +668
// 1. Match by namespace
if (rule.namespace && object?.namespace === rule.namespace) {
return rule.datasource;
}
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Namespace-based routing checks object?.namespace, but SchemaRegistry.registerObject() does not inject the manifest namespace into the object definition (examples typically omit object.namespace). This means namespace rules won’t match for most objects. Consider deriving the namespace from the resolved object’s FQN (e.g., parse object.name) or from registry contributor metadata instead of relying on object.namespace.

Copilot uses AI. Check for mistakes.
Comment on lines +620 to +623
// 3. Check package's defaultDatasource
if (object?.packageId) {
const manifest = this.manifests.get(object.packageId);
if (manifest?.defaultDatasource && manifest.defaultDatasource !== 'default') {
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This manifest-level fallback uses object?.packageId, but ServiceObject does not carry a packageId, so defaultDatasource from manifests will never be applied. Consider deriving the owning packageId from SchemaRegistry contributor metadata (e.g., owner for object.name) before reading this.manifests.

Copilot uses AI. Check for mistakes.
Comment on lines +64 to +66
// Test that it uses memory driver
const result = await engine.create('account', { name: 'Test Account' });
expect(result).toBeDefined();
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test calls engine.create(objectName, data), but ObjectQL does not expose a create method on the engine itself (only insert() on the engine and create() on ObjectRepository). This will fail to compile/run; use engine.insert(...) (or create a scoped repository) instead.

Copilot uses AI. Check for mistakes.
Comment on lines +623 to +627
if (manifest?.defaultDatasource && manifest.defaultDatasource !== 'default') {
if (this.drivers.has(manifest.defaultDatasource)) {
this.logger.debug('Resolved datasource from package manifest', {
object: objectName,
package: object.packageId,
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If manifest.defaultDatasource is configured (and not 'default') but the driver isn’t registered, this currently falls through to the global default without error. Consider throwing here as well to surface configuration mistakes early.

Copilot uses AI. Check for mistakes.
Comment on lines +72 to +76
export const DatasourceMappingRuleSchema = z.object({
/**
* Match by namespace (e.g., 'crm', 'auth', 'todo')
* Objects with this namespace will use the specified datasource.
*/
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DatasourceMappingRuleSchema doesn’t prevent rules where all selectors are omitted (namespace, package, objectPattern, default all undefined). Such rules can never match and likely indicate a config error. Consider adding a schema refinement requiring at least one selector (and optionally constraining default: true to not be combined with other selectors if you want defaults to be unambiguous).

Copilot uses AI. Check for mistakes.
Comment on lines +64 to +71
* This provides package-level datasource configuration without needing to
* specify it on every individual object.
*
* @example "memory" // Use in-memory driver for all package objects
* @example "turso" // Use Turso/LibSQL for all package objects
*/
defaultDatasource: z.string().optional().default('default')
.describe('Default datasource for all objects in this package'),
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

defaultDatasource is described as optional, but .optional().default('default') makes the post-parse output always include a value and changes the inferred ObjectStackManifest type. If the intent is “unset means global default”, consider removing the Zod default and treating undefined as 'default' in resolver logic, or adjust docs/types accordingly.

Suggested change
* This provides package-level datasource configuration without needing to
* specify it on every individual object.
*
* @example "memory" // Use in-memory driver for all package objects
* @example "turso" // Use Turso/LibSQL for all package objects
*/
defaultDatasource: z.string().optional().default('default')
.describe('Default datasource for all objects in this package'),
* If omitted in manifest input, parsing will normalize this field to `"default"`.
* As a result, parsed `ObjectStackManifest` values always include a concrete
* `defaultDatasource` string.
*
* This provides package-level datasource configuration without needing to
* specify it on every individual object.
*
* @example "memory" // Use in-memory driver for all package objects
* @example "turso" // Use Turso/LibSQL for all package objects
* @example "default" // Use the platform's default datasource
*/
defaultDatasource: z.string().default('default')
.describe('Default datasource for all objects in this package; defaults to "default" when omitted'),

Copilot uses AI. Check for mistakes.
Comment on lines +670 to +673
// 2. Match by package ID
if (rule.package && object?.packageId === rule.package) {
return rule.datasource;
}
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Package-based routing checks object?.packageId, but ServiceObject does not include packageId, so this condition will never match. Consider looking up the owning package via SchemaRegistry.getObjectOwner(fqn)?.packageId (or similar registry metadata) and compare that to rule.package.

Copilot uses AI. Check for mistakes.
Comment on lines +714 to +718
}>) {
this.datasourceMapping = rules;
this.logger.info('Datasource mapping rules configured', {
ruleCount: rules.length
});
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolveDatasourceFromMapping() clones/sorts rules and matchPattern() recompiles regexes during driver resolution, which happens on every CRUD call. Consider pre-sorting (and optionally precompiling pattern matchers) once inside setDatasourceMapping() to avoid repeated allocations and sorting overhead.

Copilot uses AI. Check for mistakes.
Comment on lines +145 to +146
* Rules are evaluated in order (or by priority if specified). First match wins.
* If no match, falls back to object's explicit `datasource` field, then 'default'.
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The datasourceMapping docstring states “If no match, falls back to object's explicit datasource field, then 'default'”, but the engine resolution order is the opposite (explicit object.datasource wins first, then mapping, then package default, then global). Please update this comment to match the actual resolution priority to avoid misleading users.

Suggested change
* Rules are evaluated in order (or by priority if specified). First match wins.
* If no match, falls back to object's explicit `datasource` field, then 'default'.
* Datasource resolution priority is:
* 1. Object's explicit `datasource` field
* 2. `datasourceMapping` rules, evaluated in order (or by priority if specified); first match wins
* 3. Package-level default datasource
* 4. Global/default datasource

Copilot uses AI. Check for mistakes.
Comment on lines +78 to +82
// Configure datasourceMapping if provided in the stack definition
if (this.bundle.datasourceMapping && Array.isArray(this.bundle.datasourceMapping)) {
ctx.logger.info('Configuring datasource mapping rules', {
appId,
ruleCount: this.bundle.datasourceMapping.length
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

datasourceMapping is applied from this.bundle.datasourceMapping (the app bundle), but the PR description/examples put datasourceMapping on the host stack (the one that lists plugins). In host/aggregator configs this won’t configure routing at all. Consider moving this wiring to host bootstrap (e.g., Runtime/ObjectQLPlugin using the root stack config) or explicitly passing host-level rules into AppPlugin instances.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants