@harborclient/sdk 0.6.15 → 0.6.16

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.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=pluginDatabaseApi.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pluginDatabaseApi.test.d.ts","sourceRoot":"","sources":["../src/pluginDatabaseApi.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,65 @@
1
+ import { describe, expect, it, jest } from '@jest/globals';
2
+ import { createPluginDatabaseApi } from './runtime/pluginDatabaseApi.js';
3
+ /**
4
+ * Builds a mock database backend for unit tests.
5
+ */
6
+ function createMockBackend() {
7
+ return {
8
+ query: jest.fn(async () => undefined),
9
+ exec: jest.fn(async () => undefined),
10
+ beginTransaction: jest.fn(async () => 'txn-1'),
11
+ endTransaction: jest.fn(async () => undefined)
12
+ };
13
+ }
14
+ describe('createPluginDatabaseApi', () => {
15
+ it('forwards get, all, and run with the correct query modes', async () => {
16
+ const backend = createMockBackend();
17
+ backend.query
18
+ .mockResolvedValueOnce({ id: 1 })
19
+ .mockResolvedValueOnce([{ id: 1 }, { id: 2 }])
20
+ .mockResolvedValueOnce({ changes: 1, lastInsertRowid: 42 });
21
+ const db = createPluginDatabaseApi(backend);
22
+ await expect(db.get('SELECT 1', [1])).resolves.toEqual({ id: 1 });
23
+ await expect(db.all('SELECT *', [2])).resolves.toEqual([{ id: 1 }, { id: 2 }]);
24
+ await expect(db.run('INSERT INTO t VALUES (?)', [3])).resolves.toEqual({
25
+ changes: 1,
26
+ lastInsertRowid: 42
27
+ });
28
+ expect(backend.query).toHaveBeenNthCalledWith(1, 'get', 'SELECT 1', [1], undefined);
29
+ expect(backend.query).toHaveBeenNthCalledWith(2, 'all', 'SELECT *', [2], undefined);
30
+ expect(backend.query).toHaveBeenNthCalledWith(3, 'run', 'INSERT INTO t VALUES (?)', [3], undefined);
31
+ });
32
+ it('forwards exec unchanged', async () => {
33
+ const backend = createMockBackend();
34
+ const db = createPluginDatabaseApi(backend);
35
+ await db.exec('CREATE TABLE t (id INTEGER)');
36
+ expect(backend.exec).toHaveBeenCalledWith('CREATE TABLE t (id INTEGER)');
37
+ });
38
+ it('commits successful transactions and passes txnId to tx helpers', async () => {
39
+ const backend = createMockBackend();
40
+ backend.query.mockResolvedValue({ changes: 1, lastInsertRowid: 1 });
41
+ const db = createPluginDatabaseApi(backend);
42
+ const result = await db.transaction(async (tx) => {
43
+ await tx.run('INSERT INTO t VALUES (?)', [1]);
44
+ await tx.all('SELECT * FROM t');
45
+ return 'done';
46
+ });
47
+ expect(result).toBe('done');
48
+ expect(backend.beginTransaction).toHaveBeenCalledTimes(1);
49
+ expect(backend.endTransaction).toHaveBeenCalledWith('txn-1', 'commit');
50
+ expect(backend.query).toHaveBeenCalledWith('run', 'INSERT INTO t VALUES (?)', [1], 'txn-1');
51
+ expect(backend.query).toHaveBeenCalledWith('all', 'SELECT * FROM t', undefined, 'txn-1');
52
+ });
53
+ it('rolls back failed transactions', async () => {
54
+ const backend = createMockBackend();
55
+ const db = createPluginDatabaseApi(backend);
56
+ const boom = new Error('boom');
57
+ await expect(db.transaction(async (tx) => {
58
+ await tx.run('INSERT INTO t VALUES (?)', [1]);
59
+ throw boom;
60
+ })).rejects.toThrow(boom);
61
+ expect(backend.beginTransaction).toHaveBeenCalledTimes(1);
62
+ expect(backend.endTransaction).toHaveBeenCalledWith('txn-1', 'rollback');
63
+ expect(backend.endTransaction).not.toHaveBeenCalledWith('txn-1', 'commit');
64
+ });
65
+ });
@@ -7,6 +7,7 @@ import {
7
7
  registerContributionHeaderActions,
8
8
  registerContributionIndicator
9
9
  } from './contributionRegistry.js';
10
+ import { createPluginDatabaseApi } from './pluginDatabaseApi.js';
10
11
  import { setHostReact } from './reactHost.js';
11
12
 
12
13
  /** @type {Map<string, Set<(...args: unknown[]) => void | Promise<void>>>} */
@@ -197,7 +198,7 @@ export function createBridgedPluginContext({ pluginId, mode, contributionId, rea
197
198
  await bridgeInvoke('storage.set', { key, value });
198
199
  }
199
200
  },
200
- database: {
201
+ database: createPluginDatabaseApi({
201
202
  query: (mode, sql, params, txnId) => {
202
203
  assertPermission('database');
203
204
  return bridgeInvoke('database.query', { mode, sql, params, txnId });
@@ -214,7 +215,7 @@ export function createBridgedPluginContext({ pluginId, mode, contributionId, rea
214
215
  assertPermission('database');
215
216
  return bridgeInvoke('database.endTransaction', { txnId, action });
216
217
  }
217
- },
218
+ }),
218
219
  fs: {
219
220
  pickFile: async (options) => {
220
221
  assertPermission('filesystem:pick');
@@ -691,7 +692,7 @@ export function mountContributionView({
691
692
  'mainViews'
692
693
  ]);
693
694
 
694
- if (FILL_SURFACE_KINDS.has(kind)) {
695
+ if (FILL_SURFACE_KINDS.has(kind) && slot === 'content') {
695
696
  document.body.classList.add('plugin-surface-fill');
696
697
  }
697
698
 
@@ -704,6 +705,15 @@ export function mountContributionView({
704
705
  root.style.overflow = 'hidden';
705
706
  }
706
707
 
708
+ if (slot === 'indicator') {
709
+ document.body.classList.add('plugin-surface-indicator');
710
+ document.documentElement.classList.add('plugin-surface-indicator');
711
+ root.style.display = 'inline-flex';
712
+ root.style.width = 'fit-content';
713
+ root.style.maxWidth = '100%';
714
+ root.style.overflow = 'hidden';
715
+ }
716
+
707
717
  /** @type {ResizeObserver | null} */
708
718
  let resizeObserver = null;
709
719
  /** @type {number | null} */
@@ -827,6 +837,60 @@ export function mountContributionView({
827
837
  };
828
838
  }
829
839
 
840
+ if (slot === 'indicator') {
841
+ /**
842
+ * Reports footer panel indicator size so the host webview stays compact inline.
843
+ */
844
+ const reportIndicatorSize = () => {
845
+ const measureTarget = root.firstElementChild ?? root;
846
+ const width = Math.ceil(
847
+ Math.max(
848
+ measureTarget.scrollWidth,
849
+ measureTarget.getBoundingClientRect().width,
850
+ measureTarget.offsetWidth
851
+ )
852
+ );
853
+ const height = Math.ceil(
854
+ Math.max(
855
+ measureTarget.scrollHeight,
856
+ measureTarget.getBoundingClientRect().height,
857
+ measureTarget.offsetHeight
858
+ )
859
+ );
860
+ if (width <= 0 && height <= 0) {
861
+ return;
862
+ }
863
+ if (resizeFrame != null) {
864
+ cancelAnimationFrame(resizeFrame);
865
+ }
866
+ resizeFrame = requestAnimationFrame(() => {
867
+ resizeFrame = requestAnimationFrame(() => {
868
+ resizeFrame = null;
869
+ void bridgeInvoke('view.reportSize', {
870
+ ...(width > 0 ? { width } : {}),
871
+ ...(height > 0 ? { height } : {}),
872
+ slot: 'indicator'
873
+ });
874
+ });
875
+ });
876
+ };
877
+
878
+ resizeObserver = new ResizeObserver(() => {
879
+ reportIndicatorSize();
880
+ });
881
+ resizeObserver.observe(root);
882
+
883
+ render();
884
+ reportIndicatorSize();
885
+
886
+ return () => {
887
+ resizeObserver?.disconnect();
888
+ if (resizeFrame != null) {
889
+ cancelAnimationFrame(resizeFrame);
890
+ }
891
+ };
892
+ }
893
+
830
894
  const unsubscribe = bridgeOn('view.context', (payload) => {
831
895
  currentContext = payload;
832
896
  render();
@@ -0,0 +1,36 @@
1
+ import type { PluginDatabase } from '../types.js';
2
+
3
+ /**
4
+ * Backend used to implement {@link PluginDatabase} in renderer or main plugin runtimes.
5
+ */
6
+ export interface PluginDatabaseBackend {
7
+ /**
8
+ * Runs one query mode, optionally inside a transaction.
9
+ */
10
+ query(
11
+ mode: 'get' | 'all' | 'run',
12
+ sql: string,
13
+ params?: unknown[],
14
+ txnId?: string
15
+ ): Promise<unknown>;
16
+
17
+ /**
18
+ * Executes a multi-statement SQL script.
19
+ */
20
+ exec(sql: string): Promise<void>;
21
+
22
+ /**
23
+ * Starts an exclusive transaction.
24
+ */
25
+ beginTransaction(): Promise<string>;
26
+
27
+ /**
28
+ * Commits or rolls back an open transaction.
29
+ */
30
+ endTransaction(txnId: string, action: 'commit' | 'rollback'): Promise<void>;
31
+ }
32
+
33
+ /**
34
+ * Builds the plugin database API surface from a runtime-specific backend.
35
+ */
36
+ export function createPluginDatabaseApi(backend: PluginDatabaseBackend): PluginDatabase;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Builds the plugin database API surface from a runtime-specific backend.
3
+ *
4
+ * @param {object} backend - IPC or child-process bridge implementing database operations.
5
+ * @param {(mode: 'get' | 'all' | 'run', sql: string, params?: unknown[], txnId?: string) => Promise<unknown>} backend.query
6
+ * @param {(sql: string) => Promise<void>} backend.exec
7
+ * @param {() => Promise<string>} backend.beginTransaction
8
+ * @param {(txnId: string, action: 'commit' | 'rollback') => Promise<void>} backend.endTransaction
9
+ * @returns {import('../types.js').PluginDatabase}
10
+ */
11
+ export function createPluginDatabaseApi(backend) {
12
+ /**
13
+ * Runs one query mode through the backend.
14
+ *
15
+ * @param {'get' | 'all' | 'run'} mode - Query shape to execute.
16
+ * @param {string} sql - Parameterized SQL statement.
17
+ * @param {unknown[]} [params] - Bound parameter values.
18
+ * @param {string} [txnId] - Active transaction id when applicable.
19
+ * @returns {Promise<unknown>}
20
+ */
21
+ const query = (mode, sql, params, txnId) => backend.query(mode, sql, params, txnId);
22
+
23
+ /**
24
+ * Transaction-scoped helpers passed to {@link PluginDatabase.transaction}.
25
+ *
26
+ * @param {string} txnId - Active transaction id.
27
+ * @returns {import('../types.js').PluginDatabaseTx}
28
+ */
29
+ const createTx = (txnId) => ({
30
+ get: async (sql, params) => query('get', sql, params, txnId),
31
+ all: async (sql, params) => query('all', sql, params, txnId),
32
+ run: async (sql, params) => query('run', sql, params, txnId)
33
+ });
34
+
35
+ return {
36
+ get: async (sql, params) => query('get', sql, params),
37
+ all: async (sql, params) => query('all', sql, params),
38
+ run: async (sql, params) => query('run', sql, params),
39
+ exec: (sql) => backend.exec(sql),
40
+ transaction: async (fn) => {
41
+ const txnId = await backend.beginTransaction();
42
+ try {
43
+ const result = await fn(createTx(txnId));
44
+ await backend.endTransaction(txnId, 'commit');
45
+ return result;
46
+ } catch (error) {
47
+ await backend.endTransaction(txnId, 'rollback');
48
+ throw error;
49
+ }
50
+ }
51
+ };
52
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harborclient/sdk",
3
- "version": "0.6.15",
3
+ "version": "0.6.16",
4
4
  "description": "TypeScript SDK for HarborClient plugin development.",
5
5
  "keywords": [
6
6
  "harborclient",