@kahitsan/plugin-sdk 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kahitsan/plugin-sdk",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "THE single public author surface for Hilinga plugins: createPlugin, auth guards, the workspace-scoped data surface (makeDataSurface) + migrations + withTenantContext, cross-plugin RPC, and the wire-protocol verify surface. One npm package, no -composite/-protocol companions. Self-contained, no kernel internals, no signing capability.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -182,6 +182,21 @@ function quote(name: string): string {
182
182
  return `"${name.replace(/"/g, '""')}"`;
183
183
  }
184
184
 
185
+ /** For a degraded bare-aggregate read (no GROUP BY), return a single zero row keyed by
186
+ * the projected column name so `rows[0].count` resolves to 0 instead of throwing.
187
+ * Returns null when the SQL isn't a bare aggregate (caller then degrades to []). */
188
+ function aggregateZeroRow<R>(sql: string): QueryResult<R> | null {
189
+ if (/\bGROUP\s+BY\b/i.test(sql)) return null;
190
+ const m = /^\s*SELECT\s+(.+?)\s+FROM\b/is.exec(sql);
191
+ if (!m) return null;
192
+ const proj = m[1].trim();
193
+ const fn = /^(count|sum|avg|min|max|coalesce)\s*\(/i.exec(proj);
194
+ if (!fn) return null;
195
+ const aliasM = /\bAS\s+"?([A-Za-z_][A-Za-z0-9_]*)"?\s*$/i.exec(proj);
196
+ const alias = aliasM ? aliasM[1] : fn[1].toLowerCase();
197
+ return { rows: [{ [alias]: 0 } as unknown as R], rowCount: 1 };
198
+ }
199
+
185
200
  /** Classify a SQLite execution error into a tolerable empty result (Vision §5.1
186
201
  * resilience), or null to rethrow. Tolerated: an idempotent re-`ADD COLUMN`; any
187
202
  * statement during the boot migration window or any DDL (best-effort — the plugin
@@ -198,7 +213,15 @@ function tolerateError<R>(sql: string, err: Error): QueryResult<R> | null {
198
213
  if (/duplicate column name/i.test(msg) && /\bADD\s+COLUMN\b/i.test(sql)) return empty;
199
214
  const migrating = (globalThis as { __KSERP_DEV_MIGRATING__?: boolean }).__KSERP_DEV_MIGRATING__;
200
215
  if (migrating || IS_DDL.test(sql)) return (debug("skipped a Postgres-only statement"), empty);
201
- if (RETURNS_ROWS.test(sql)) return (debug("degraded a Postgres-only read (empty result)"), empty);
216
+ if (RETURNS_ROWS.test(sql)) {
217
+ // A bare aggregate SELECT (COUNT/SUM/… with no GROUP BY) ALWAYS returns exactly one
218
+ // row in Postgres (0/NULL over an empty set). Degrading it to [] crashes the common
219
+ // list-count handler `rows[0].count`, so hand back a single zero row keyed by the
220
+ // projected column (explicit `AS alias`, else pg's default = the function name).
221
+ const agg = aggregateZeroRow<R>(sql);
222
+ if (agg) return (debug("degraded a Postgres-only aggregate (zero row)"), agg);
223
+ return (debug("degraded a Postgres-only read (empty result)"), empty);
224
+ }
202
225
  if (process.env.KSERP_DEV_SQL_DEBUG)
203
226
  console.error(`[dev-stub sqlite] FAILED SQL:\n${sql}\n→ ${msg}`);
204
227
  return null;