@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.
- package/dist/pluginDatabaseApi.test.d.ts +2 -0
- package/dist/pluginDatabaseApi.test.d.ts.map +1 -0
- package/dist/pluginDatabaseApi.test.js +65 -0
- package/dist/runtime/createBridgedPluginContext.js +67 -3
- package/dist/runtime/pluginDatabaseApi.d.ts +36 -0
- package/dist/runtime/pluginDatabaseApi.js +52 -0
- package/package.json +1 -1
|
@@ -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
|
+
}
|