Skip to content

feat: implement @objectstack/service-analytics with multi-driver strategy pattern#946

Merged
hotlong merged 4 commits into
mainfrom
copilot/implement-service-analytics-plugin
Mar 22, 2026
Merged

feat: implement @objectstack/service-analytics with multi-driver strategy pattern#946
hotlong merged 4 commits into
mainfrom
copilot/implement-service-analytics-plugin

Conversation

Copilot AI commented Mar 22, 2026

Copy link
Copy Markdown
Contributor

Analytics only had MemoryAnalyticsService — no path to Postgres/Mongo/SQL-backed aggregation. This adds @objectstack/service-analytics as the production IAnalyticsService implementation with driver-aware strategy dispatch.

Architecture

  • AnalyticsService — Orchestrator that walks a priority-ordered strategy chain:
    • P1 NativeSQLStrategy — Pushes native SQL to SQL-capable drivers (Postgres, MySQL)
    • P2 ObjectQLStrategy — Translates to engine.aggregate() AST for any ObjectQL driver
    • P3 FallbackDelegateStrategy (internal) — Auto-added when fallbackService is configured; delegates to any pre-registered IAnalyticsService
  • InMemoryStrategy — Defined in @objectstack/driver-memory (not in service-analytics), for explicit use with the memory driver
  • Strategy contract types (AnalyticsStrategy, StrategyContext, DriverCapabilities) — Defined in @objectstack/spec/contracts/analytics-service.ts following Protocol First principle
  • CubeRegistry — Collects cube definitions from manifests + auto-infers from object schemas (numeric → sum/avg measures, date → time dimensions)
  • AnalyticsServicePlugin — Kernel plugin lifecycle; auto-detects pre-registered analytics service as fallback via replaceService. Analytics service registration is owned by service-analytics, not by any driver.
  • DriverCapabilities probing (nativeSql, objectqlAggregate, inMemory) drives strategy selection at runtime

Usage

const kernel = new LiteKernel();
kernel.use(new AnalyticsServicePlugin({
  cubes: [ordersCube],
  queryCapabilities: () => ({ nativeSql: true, objectqlAggregate: true, inMemory: false }),
  executeRawSql: (obj, sql, params) => pgPool.query(sql, params).then(r => r.rows),
}));
await kernel.bootstrap();

const analytics = kernel.getService<IAnalyticsService>('analytics');
await analytics.query({ cube: 'orders', measures: ['orders.count'], dimensions: ['orders.status'] });
await analytics.generateSql({ cube: 'orders', measures: ['orders.total_amount'] }); // dry-run

Files

  • packages/services/service-analytics/ — Orchestrator, NativeSQLStrategy, ObjectQLStrategy, cube registry, plugin, barrel exports
  • packages/plugins/driver-memory/src/in-memory-strategy.ts — InMemoryStrategy (moved here from service-analytics per architectural feedback)
  • packages/spec/src/contracts/analytics-service.ts — Strategy contract types added alongside IAnalyticsService
  • 30 unit tests in service-analytics covering strategy branches, registry inference, orchestrator fallback chain
  • ROADMAP.md — Analytics service status updated from 🟡 → ✅
  • CHANGELOG.md — Documented under [Unreleased]
Original prompt

This section details on the original issue you should resolve

<issue_title>实现 service-analytics,支持多数据库驱动的分析服务</issue_title>
<issue_description>## 背景
当前 ObjectStack 的 analytics 接口仅有 MemoryAnalyticsService 实现,无法支撑 PostgreSQL/Mongo/SQL 驱动的多数据源分析能力。

目标

  • packages/services/service-analytics 下实现标准化 service-analytics 插件。
  • 提供 IAnalyticsService 的完整实现,支持基于任意 Driver(Postgres、Mongo、Memory)进行分析聚合。
  • 支持自动发现和注册 Cube(支持 manifest 中明确定义和自动从对象推导)。
  • 通过策略模式(Strategy Pattern)根据 driver 能力动态下发分析查询(ObjectQL、原生 SQL、Memory 等)。
  • 提供原生 SQL 解释/生成能力(generateSql)。

关键方案

  • 架构分离:AnalyticsService 仅为 orchestrator,实际执行通过策略链(Strategy 模式)委托给不同的实现
    • P1:NativeSQLStrategy(优先下推原生 SQL)
    • P2:ObjectQLStrategy(通用逻辑在 driver 支持时回落)
    • P3:InMemoryStrategy(只限于 Memory/Dev 环境)
  • CubeRegistry 自动收集全部 cubes,支持多 manifest 来源
  • 完善跨驱动能力探测(queryAggregations),以适配当前和未来的多种存储后端
  • 确保与 API 协议 / Kernel 插件机制一致对齐

验收标准

  • 可以通过 objectql+postgres 或 objectql+mongo 驱动完成 /api/v1/analytics/query 查询(聚合、分组、过滤)
  • 各驱动的聚合能力可动态探测与切换
  • dev/test 环境继续兼容 MemoryAnalyticsService ��为
  • 有单元测试覆盖主要策略分支
  • ROADMAP.md、CHANGELOG.md 更新

本需求为实施多驱动分析能力的基础。相关配套 PR 会同步在 objectui 完成 UI 注册和调用。</issue_description>

Comments on the Issue (you are @copilot in this section)


⚡ Quickly spin up Copilot coding agent tasks from anywhere on your macOS or Windows machine with Raycast.

@vercel

vercel Bot commented Mar 22, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
objectstack-play Ready Ready Preview, Comment Mar 22, 2026 4:41am
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
spec Ignored Ignored Mar 22, 2026 4:41am

Request Review

…tegy pattern

- AnalyticsService orchestrator implementing IAnalyticsService
- Strategy pattern: NativeSQLStrategy (P1), ObjectQLStrategy (P2), InMemoryStrategy (P3)
- CubeRegistry with auto-inference from object schemas
- AnalyticsServicePlugin for kernel lifecycle
- generateSql() dry-run SQL generation
- 34 unit tests covering all strategy branches
- Updated ROADMAP.md and CHANGELOG.md

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Agent-Logs-Url: https://github.com/objectstack-ai/spec/sessions/ad90f5d5-0180-49f2-9b9b-72bf2792ed93
… redundant ternary

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Agent-Logs-Url: https://github.com/objectstack-ai/spec/sessions/ad90f5d5-0180-49f2-9b9b-72bf2792ed93
Copilot AI changed the title [WIP] Add implementation for service-analytics to support multiple database drivers feat: implement @objectstack/service-analytics with multi-driver strategy pattern Mar 22, 2026
Copilot AI requested a review from hotlong March 22, 2026 04:10
@hotlong

hotlong commented Mar 22, 2026

Copy link
Copy Markdown
Collaborator

@copilot InMemoryStrategy 是否应该在 memory driver 中定义。还有 注册 analytics 服务是否也应该在service-analytics, 而不是 memory driver ?

…to spec

- Move AnalyticsStrategy, StrategyContext, DriverCapabilities interfaces
  to @objectstack/spec/contracts (Protocol First principle)
- Move InMemoryStrategy from service-analytics to driver-memory
- Replace built-in InMemoryStrategy with internal FallbackDelegateStrategy
  in AnalyticsService (auto-added when fallbackService is configured)
- service-analytics/strategies/types.ts now re-exports from spec
- Analytics service registration remains in service-analytics (already correct)
- All tests pass: 30 service-analytics, 94 driver-memory, 6704 spec

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Agent-Logs-Url: https://github.com/objectstack-ai/spec/sessions/94c99192-9b5e-4b97-809a-902bc64293c3

Copilot AI commented Mar 22, 2026

Copy link
Copy Markdown
Contributor Author

@copilot InMemoryStrategy 是否应该在 memory driver 中定义。还有 注册 analytics 服务是否也应该在service-analytics, 而不是 memory driver ?

Agreed on both points. In ac2177b:

  1. InMemoryStrategy moved to driver-memorypackages/plugins/driver-memory/src/in-memory-strategy.ts, exported from the driver-memory index. The strategy contract types (AnalyticsStrategy, StrategyContext, DriverCapabilities) are now defined in @objectstack/spec/contracts/analytics-service.ts following Protocol First principle.

  2. Analytics service registration stays in service-analytics — this was already the case (plugin.ts does ctx.registerService('analytics', ...) / ctx.replaceService). The AnalyticsService now uses an internal FallbackDelegateStrategy (not exported) that auto-activates when fallbackService is configured, so it no longer depends on InMemoryStrategy from driver-memory.

@hotlong
hotlong marked this pull request as ready for review March 22, 2026 04:46
Copilot AI review requested due to automatic review settings March 22, 2026 04:46
@hotlong
hotlong merged commit 84042de into main Mar 22, 2026
5 checks passed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds @objectstack/service-analytics as a production-ready IAnalyticsService implementation, introducing a multi-driver strategy chain (Native SQL / ObjectQL / fallback delegation) and a cube registry to enable cross-driver analytics querying and SQL generation.

Changes:

  • Introduces AnalyticsService orchestrator + AnalyticsServicePlugin, with a priority-ordered strategy chain.
  • Adds NativeSQLStrategy and ObjectQLStrategy, plus a CubeRegistry with object-field inference.
  • Extends spec contracts with strategy-chain types and exports an InMemoryStrategy from @objectstack/driver-memory; updates changelogs/roadmap.

Reviewed changes

Copilot reviewed 16 out of 17 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
ROADMAP.md Marks analytics service as completed and documents new capabilities/tests.
pnpm-lock.yaml Adds lockfile entries for the new service package.
packages/spec/src/contracts/analytics-service.ts Adds strategy-pattern contract types alongside IAnalyticsService.
packages/services/service-analytics/tsconfig.json New TS config for the service package.
packages/services/service-analytics/src/strategies/types.ts Re-exports strategy contract types from @objectstack/spec/contracts.
packages/services/service-analytics/src/strategies/objectql-strategy.ts Implements ObjectQL-based aggregation strategy.
packages/services/service-analytics/src/strategies/native-sql-strategy.ts Implements native SQL pushdown strategy + SQL generator.
packages/services/service-analytics/src/plugin.ts Adds kernel plugin that registers/replaces the analytics service with fallback capture.
packages/services/service-analytics/src/index.ts Barrel exports for service/plugin/registry/strategies.
packages/services/service-analytics/src/cube-registry.ts Adds cube registry + schema inference helper.
packages/services/service-analytics/src/analytics-service.ts Orchestrator that selects strategies and exposes query/getMeta/generateSql.
packages/services/service-analytics/src/tests/analytics-service.test.ts Unit tests for registry, strategies, fallback delegation, orchestration.
packages/services/service-analytics/package.json New package manifest for @objectstack/service-analytics.
packages/services/service-analytics/CHANGELOG.md New package changelog entry documenting the initial release.
packages/plugins/driver-memory/src/index.ts Exports InMemoryStrategy from the memory driver package.
packages/plugins/driver-memory/src/in-memory-strategy.ts Adds an in-memory fallback-delegating strategy implementation.
CHANGELOG.md Documents the new analytics service in the monorepo changelog.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Comment on lines +21 to +25
return caps.objectqlAggregate && typeof ctx.executeAggregate === 'function';
}

async execute(query: AnalyticsQuery, ctx: StrategyContext): Promise<AnalyticsResult> {
const cube = ctx.getCube(query.cube!)!;

Copilot AI Mar 22, 2026

Copy link

Choose a reason for hiding this comment

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

canHandle() doesn't verify that the cube is actually registered/available via ctx.getCube(). If queryCapabilities() returns objectqlAggregate: true for an unknown cube, this strategy will be selected and execute() will crash on ctx.getCube(query.cube!)!. Consider returning false when ctx.getCube(query.cube) is undefined, and have execute() throw a clear "Cube not found" error instead of relying on non-null assertions (this also allows fallback strategies to run for unknown cubes).

Suggested change
return caps.objectqlAggregate && typeof ctx.executeAggregate === 'function';
}
async execute(query: AnalyticsQuery, ctx: StrategyContext): Promise<AnalyticsResult> {
const cube = ctx.getCube(query.cube!)!;
if (!caps.objectqlAggregate || typeof ctx.executeAggregate !== 'function') {
return false;
}
if (typeof ctx.getCube !== 'function') {
return false;
}
const cube = ctx.getCube(query.cube);
if (!cube) {
return false;
}
return true;
}
async execute(query: AnalyticsQuery, ctx: StrategyContext): Promise<AnalyticsResult> {
if (!query.cube) {
throw new Error('Cube not specified in analytics query');
}
if (typeof ctx.getCube !== 'function') {
throw new Error('Cube registry is not available in strategy context');
}
const cube = ctx.getCube(query.cube);
if (!cube) {
throw new Error(`Cube not found: ${query.cube}`);
}

Copilot uses AI. Check for mistakes.
Comment on lines +142 to +154
if (operator === 'set') return { $ne: null };
if (operator === 'notSet') return null;
if (!values || values.length === 0) return undefined;

switch (operator) {
case 'equals': return values[0];
case 'notEquals': return { $ne: values[0] };
case 'gt': return { $gt: values[0] };
case 'gte': return { $gte: values[0] };
case 'lt': return { $lt: values[0] };
case 'lte': return { $lte: values[0] };
case 'contains': return { $regex: values[0] };
default: return values[0];

Copilot AI Mar 22, 2026

Copy link

Choose a reason for hiding this comment

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

convertFilter() is emitting Mongo/Mingo-style operators like $regex and returning raw values, but ObjectQL filter conventions in this repo use $contains/$notContains (and then drivers normalize as needed). Using $regex directly is likely to break on non-memory drivers, and returning undefined can leave filter[field]=undefined while still sending a non-empty filter object. Suggest mapping to the canonical operators ($eq, $ne, $gt, $gte, $lt, $lte, $contains, $notContains, etc.) and skipping filters that have no effective value.

Suggested change
if (operator === 'set') return { $ne: null };
if (operator === 'notSet') return null;
if (!values || values.length === 0) return undefined;
switch (operator) {
case 'equals': return values[0];
case 'notEquals': return { $ne: values[0] };
case 'gt': return { $gt: values[0] };
case 'gte': return { $gte: values[0] };
case 'lt': return { $lt: values[0] };
case 'lte': return { $lte: values[0] };
case 'contains': return { $regex: values[0] };
default: return values[0];
// Canonical "is set" / "is not set" semantics
if (operator === 'set') return { $ne: null };
if (operator === 'notSet') return { $eq: null };
// No effective filter if there are no values; let caller decide whether to skip
if (!values || values.length === 0) return undefined;
switch (operator) {
case 'equals':
return { $eq: values[0] };
case 'notEquals':
return { $ne: values[0] };
case 'gt':
return { $gt: values[0] };
case 'gte':
return { $gte: values[0] };
case 'lt':
return { $lt: values[0] };
case 'lte':
return { $lte: values[0] };
case 'contains':
return { $contains: values[0] };
case 'notContains':
return { $notContains: values[0] };
default:
// Unknown operator: no filter emitted; caller should skip
return undefined;

Copilot uses AI. Check for mistakes.
if (query.measures) {
for (const m of query.measures) {
const { field, method } = this.resolveMeasureAggregation(cube, m);
const aggSql = method === 'count' ? 'COUNT(*)' : `${method.toUpperCase()}(${field})`;

Copilot AI Mar 22, 2026

Copy link

Choose a reason for hiding this comment

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

generateSql() renders count_distinct as COUNT_DISTINCT(field) via method.toUpperCase(), which isn't valid SQL; it should be represented as COUNT(DISTINCT field). Consider special-casing count_distinct similarly to how NativeSQLStrategy does, so generateSql() outputs syntactically correct SQL for that measure type.

Suggested change
const aggSql = method === 'count' ? 'COUNT(*)' : `${method.toUpperCase()}(${field})`;
const aggSql =
method === 'count'
? 'COUNT(*)'
: method === 'count_distinct'
? `COUNT(DISTINCT ${field})`
: `${method.toUpperCase()}(${field})`;

Copilot uses AI. Check for mistakes.
}
}

private extractObjectName(cube: Cube): string {

Copilot AI Mar 22, 2026

Copy link

Choose a reason for hiding this comment

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

extractObjectName() returns cube.sql.trim(), but CubeSchema.sql is documented as "Base SQL statement or Table Name". For the ObjectQL aggregate path, the engine typically expects an ObjectQL object name (likely cube.name), not an arbitrary SQL string. If cube.sql is a SELECT/subquery or differs from cube.name, executeAggregate() will be invoked with an invalid object name.

Suggested change
private extractObjectName(cube: Cube): string {
private extractObjectName(cube: Cube): string {
// Prefer the logical cube name for ObjectQL object resolution.
if (cube.name && cube.name.trim().length > 0) {
return cube.name.trim();
}
// Fallback to the SQL/table name only if no explicit name is provided.

Copilot uses AI. Check for mistakes.
Comment on lines +88 to +95
const tableName = this.extractObjectName(cube);
let sql = `SELECT ${selectClauses.join(', ')} FROM "${tableName}"`;
if (whereClauses.length > 0) {
sql += ` WHERE ${whereClauses.join(' AND ')}`;
}
if (groupByClauses.length > 0) {
sql += ` GROUP BY ${groupByClauses.join(', ')}`;
}

Copilot AI Mar 22, 2026

Copy link

Choose a reason for hiding this comment

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

CubeSchema.sql is defined as "Base SQL statement or Table Name", but this strategy treats cube.sql as a simple table identifier and wraps it as FROM "${tableName}". This will break for valid cube definitions where sql is a full SELECT/join or already-quoted/qualified identifier, and it also prevents using schema.table without changing semantics. Consider supporting cube.sql as an arbitrary SQL source (e.g., FROM (${cube.sql}) AS <alias>) or clearly restricting/validating it to a safe table name before quoting.

Copilot uses AI. Check for mistakes.
Comment on lines +147 to +152
async getMeta(cubeName?: string): Promise<CubeMeta[]> {
// If a fallback service is configured, merge its metadata with the registry
const cubes = cubeName
? [this.cubeRegistry.get(cubeName)].filter(Boolean) as Cube[]
: this.cubeRegistry.getAll();

Copilot AI Mar 22, 2026

Copy link

Choose a reason for hiding this comment

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

getMeta() comment says fallback metadata is merged, but the implementation ignores fallbackService entirely. In the common case where AnalyticsServicePlugin replaces an existing (memory) analytics service as fallbackService without passing cubes, getMeta() will return [] even though queries may still work via fallback delegation. Consider merging await fallbackService.getMeta(cubeName) with cubeRegistry metadata (dedupe by cube name), or at least documenting that callers must register cubes in cubeRegistry for discovery.

Copilot uses AI. Check for mistakes.
Comment on lines +64 to +67
* - `number` fields → `sum`, `avg`, `min`, `max` measures
* - `boolean` fields → `count` measure (count where true)
* - All non-computed fields → dimensions
* - `date`/`datetime` fields → time dimensions with standard granularities

Copilot AI Mar 22, 2026

Copy link

Choose a reason for hiding this comment

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

The heuristic rules in the doc comment don't match the implementation: it claims boolean fields become a "count where true" measure and numeric fields get sum/avg/min/max, but the code only adds dimensions for booleans and only creates sum and avg measures for numeric fields. Either update the comment to reflect current behavior or implement the missing measure inference to avoid misleading consumers.

Suggested change
* - `number` fields `sum`, `avg`, `min`, `max` measures
* - `boolean` fields `count` measure (count where true)
* - All non-computed fields dimensions
* - `date`/`datetime` fields time dimensions with standard granularities
* - `number`/`currency`/`percent` fields `sum`, `avg` measures
* - All fields dimensions (typed via `fieldTypeToDimensionType`)
* - `date`/`datetime` fields time dimensions with standard granularities
* - `boolean` fields boolean dimensions (no automatic measures)

Copilot uses AI. Check for mistakes.
Comment on lines +110 to +112
ctx.hook('analytics:beforeQuery', async (query: unknown) => {
ctx.logger.debug('[Analytics] Before query', { query });
});

Copilot AI Mar 22, 2026

Copy link

Choose a reason for hiding this comment

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

debug mode registers an analytics:beforeQuery hook, but there are no ctx.trigger('analytics:beforeQuery', ...) call sites in the repo. As a result this hook never runs and the option is effectively dead. Consider either removing this hook registration or adding an explicit trigger in the analytics request path (e.g., wherever IAnalyticsService.query() is invoked) so the debug logging actually executes.

Suggested change
ctx.hook('analytics:beforeQuery', async (query: unknown) => {
ctx.logger.debug('[Analytics] Before query', { query });
});
ctx.logger.debug('[Analytics] Debug mode enabled');
const cubeNames = this.options.cubes?.map((cube) => cube.name) ?? [];
ctx.logger.debug('[Analytics] Registered cubes', { cubes: cubeNames });

Copilot uses AI. Check for mistakes.
Comment on lines +11 to +14
- Strategy pattern with priority chain:
- **P1 — NativeSQLStrategy**: Pushes queries as native SQL to SQL-capable drivers (Postgres, MySQL, etc.)
- **P2 — ObjectQLStrategy**: Translates analytics queries into ObjectQL `engine.aggregate()` calls
- **P3 — InMemoryStrategy**: Delegates to any registered `IAnalyticsService` (e.g., `MemoryAnalyticsService`)

Copilot AI Mar 22, 2026

Copy link

Choose a reason for hiding this comment

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

The changelog entry lists P3 — InMemoryStrategy as part of @objectstack/service-analytics, but in this PR InMemoryStrategy lives in @objectstack/driver-memory and the service itself auto-adds an internal FallbackDelegateStrategy. To avoid confusing consumers, consider updating the changelog wording to reflect the actual exported/public strategies and the fallback delegation behavior.

Suggested change
- Strategy pattern with priority chain:
- **P1 — NativeSQLStrategy**: Pushes queries as native SQL to SQL-capable drivers (Postgres, MySQL, etc.)
- **P2 — ObjectQLStrategy**: Translates analytics queries into ObjectQL `engine.aggregate()` calls
- **P3 — InMemoryStrategy**: Delegates to any registered `IAnalyticsService` (e.g., `MemoryAnalyticsService`)
- Strategy pattern with priority chain (built-in strategies):
- **P1 — NativeSQLStrategy**: Pushes queries as native SQL to SQL-capable drivers (Postgres, MySQL, etc.)
- **P2 — ObjectQLStrategy**: Translates analytics queries into ObjectQL `engine.aggregate()` calls
- **P3 — FallbackDelegateStrategy**: Internal delegating strategy that forwards to any additional `IAnalyticsService` implementations (for example, `InMemoryStrategy` from `@objectstack/driver-memory`)

Copilot uses AI. Check for mistakes.
Comment on lines +21 to +25
// Can handle when a fallback service exists
if (ctx.fallbackService) return true;
// Or when the driver is flagged as in-memory
const caps = ctx.queryCapabilities(query.cube);
return caps.inMemory;

Copilot AI Mar 22, 2026

Copy link

Choose a reason for hiding this comment

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

canHandle() returns true when caps.inMemory is true even if ctx.fallbackService is missing, but execute() will then throw "No fallback analytics service available". This makes the strategy claim it can handle a query when it cannot. Consider making canHandle() require ctx.fallbackService (or alternatively implement a non-fallback execution path for true in-memory drivers).

Suggested change
// Can handle when a fallback service exists
if (ctx.fallbackService) return true;
// Or when the driver is flagged as in-memory
const caps = ctx.queryCapabilities(query.cube);
return caps.inMemory;
// Strategy can only operate when a fallback service is available
const caps = ctx.queryCapabilities(query.cube);
// Prefer in-memory drivers, but always require a fallback service
if (caps.inMemory && ctx.fallbackService) return true;
// Fallback-only path (preserves existing behavior when a fallback is registered)
return !!ctx.fallbackService;

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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

实现 service-analytics,支持多数据库驱动的分析服务

3 participants