@kubun/store-connector 0.9.0 → 0.11.0
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/lib/api.d.ts +15 -10
- package/lib/api.js +152 -1
- package/lib/definition.js +7 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.js +6 -1
- package/lib/migrations.d.ts +1 -1
- package/lib/migrations.js +34 -1
- package/lib/tables.d.ts +10 -0
- package/lib/tables.js +2 -1
- package/package.json +8 -6
package/lib/api.d.ts
CHANGED
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
import type { Adapter } from '@kubun/db-adapter';
|
|
2
2
|
import type { Kysely } from 'kysely';
|
|
3
3
|
import type { ConnectorTables } from './tables.js';
|
|
4
|
-
export type Credential = {
|
|
5
|
-
accessToken: string;
|
|
6
|
-
refreshToken?: string;
|
|
7
|
-
expiresAt?: Date;
|
|
8
|
-
scopes: Array<string>;
|
|
9
|
-
accountLabel?: string;
|
|
10
|
-
metadata?: Record<string, unknown>;
|
|
11
|
-
};
|
|
12
4
|
export type SyncStatus = 'idle' | 'syncing' | 'error';
|
|
13
5
|
export type SyncState = {
|
|
14
6
|
checkpoint: unknown;
|
|
@@ -16,18 +8,31 @@ export type SyncState = {
|
|
|
16
8
|
entityCount: number;
|
|
17
9
|
status: SyncStatus;
|
|
18
10
|
error?: string;
|
|
11
|
+
leaseExpiresAt?: number | null;
|
|
19
12
|
};
|
|
20
13
|
export type SyncStateEntry = SyncState & {
|
|
21
14
|
connectorName: string;
|
|
22
15
|
ownerDID: string;
|
|
23
16
|
};
|
|
17
|
+
export type PendingAuthRecord = {
|
|
18
|
+
state: string;
|
|
19
|
+
codeVerifier: string;
|
|
20
|
+
provider: string;
|
|
21
|
+
ownerDID: string;
|
|
22
|
+
createdAt: number;
|
|
23
|
+
};
|
|
24
24
|
export type ConnectorStoreAPI = {
|
|
25
|
-
getCredential(providerName: string, ownerDID: string): Promise<
|
|
26
|
-
setCredential(providerName: string, ownerDID: string, credential:
|
|
25
|
+
getCredential(providerName: string, ownerDID: string): Promise<string | null>;
|
|
26
|
+
setCredential(providerName: string, ownerDID: string, credential: string): Promise<void>;
|
|
27
27
|
deleteCredential(providerName: string, ownerDID: string): Promise<void>;
|
|
28
28
|
getSyncState(connectorName: string, ownerDID: string): Promise<SyncState | null>;
|
|
29
29
|
setSyncState(connectorName: string, ownerDID: string, state: SyncState): Promise<void>;
|
|
30
30
|
deleteSyncState(connectorName: string, ownerDID: string): Promise<void>;
|
|
31
31
|
listSyncStatesForConnector(connectorName: string): Promise<Array<SyncStateEntry>>;
|
|
32
|
+
claimSync(connectorName: string, ownerDID: string, leaseMs: number): Promise<boolean>;
|
|
33
|
+
renewSync(connectorName: string, ownerDID: string, leaseMs: number): Promise<void>;
|
|
34
|
+
createPendingAuth(record: PendingAuthRecord): Promise<void>;
|
|
35
|
+
consumePendingAuth(state: string): Promise<PendingAuthRecord | null>;
|
|
36
|
+
deleteExpiredPendingAuth(cutoffMs: number): Promise<void>;
|
|
32
37
|
};
|
|
33
38
|
export declare function createConnectorStore(db: Kysely<ConnectorTables>, _adapter: Adapter): ConnectorStoreAPI;
|
package/lib/api.js
CHANGED
|
@@ -1 +1,152 @@
|
|
|
1
|
-
|
|
1
|
+
// --- Serialization helpers ---
|
|
2
|
+
function rowToSyncState(row) {
|
|
3
|
+
// ParseJSONResultsPlugin may have already parsed the checkpoint value
|
|
4
|
+
let checkpoint = null;
|
|
5
|
+
if (row.checkpoint != null) {
|
|
6
|
+
checkpoint = typeof row.checkpoint === 'string' ? JSON.parse(row.checkpoint) : row.checkpoint;
|
|
7
|
+
}
|
|
8
|
+
const state = {
|
|
9
|
+
checkpoint,
|
|
10
|
+
lastSyncedAt: row.last_synced_at,
|
|
11
|
+
entityCount: row.entity_count,
|
|
12
|
+
status: row.status,
|
|
13
|
+
leaseExpiresAt: row.lease_expires_at
|
|
14
|
+
};
|
|
15
|
+
if (row.error != null) {
|
|
16
|
+
state.error = row.error;
|
|
17
|
+
}
|
|
18
|
+
return state;
|
|
19
|
+
}
|
|
20
|
+
// --- Factory function ---
|
|
21
|
+
export function createConnectorStore(db, _adapter) {
|
|
22
|
+
return {
|
|
23
|
+
// --- Credential operations ---
|
|
24
|
+
async getCredential (providerName, ownerDID) {
|
|
25
|
+
const row = await db.selectFrom('kubun_connector_credentials').select('credential').where('provider_name', '=', providerName).where('owner_did', '=', ownerDID).executeTakeFirst();
|
|
26
|
+
if (row == null) return null;
|
|
27
|
+
return row.credential;
|
|
28
|
+
},
|
|
29
|
+
async setCredential (providerName, ownerDID, credential) {
|
|
30
|
+
await db.insertInto('kubun_connector_credentials').values({
|
|
31
|
+
provider_name: providerName,
|
|
32
|
+
owner_did: ownerDID,
|
|
33
|
+
credential
|
|
34
|
+
}).onConflict((oc)=>oc.columns([
|
|
35
|
+
'provider_name',
|
|
36
|
+
'owner_did'
|
|
37
|
+
]).doUpdateSet({
|
|
38
|
+
credential
|
|
39
|
+
})).execute();
|
|
40
|
+
},
|
|
41
|
+
async deleteCredential (providerName, ownerDID) {
|
|
42
|
+
await db.deleteFrom('kubun_connector_credentials').where('provider_name', '=', providerName).where('owner_did', '=', ownerDID).execute();
|
|
43
|
+
},
|
|
44
|
+
// --- Sync state operations ---
|
|
45
|
+
async getSyncState (connectorName, ownerDID) {
|
|
46
|
+
const row = await db.selectFrom('kubun_connector_sync_state').selectAll().where('connector_name', '=', connectorName).where('owner_did', '=', ownerDID).executeTakeFirst();
|
|
47
|
+
if (row == null) return null;
|
|
48
|
+
return rowToSyncState(row);
|
|
49
|
+
},
|
|
50
|
+
async setSyncState (connectorName, ownerDID, state) {
|
|
51
|
+
// Progress write only: the lease column is owned by claimSync/renewSync,
|
|
52
|
+
// never by a plain set. A fresh INSERT carries no lease (null); on
|
|
53
|
+
// conflict the lease is deliberately excluded so a live lease held by the
|
|
54
|
+
// sync worker survives progress updates.
|
|
55
|
+
await db.insertInto('kubun_connector_sync_state').values({
|
|
56
|
+
connector_name: connectorName,
|
|
57
|
+
owner_did: ownerDID,
|
|
58
|
+
checkpoint: state.checkpoint != null ? JSON.stringify(state.checkpoint) : null,
|
|
59
|
+
last_synced_at: state.lastSyncedAt,
|
|
60
|
+
entity_count: state.entityCount,
|
|
61
|
+
status: state.status,
|
|
62
|
+
error: state.error ?? null,
|
|
63
|
+
lease_expires_at: null
|
|
64
|
+
}).onConflict((oc)=>oc.columns([
|
|
65
|
+
'connector_name',
|
|
66
|
+
'owner_did'
|
|
67
|
+
]).doUpdateSet({
|
|
68
|
+
checkpoint: state.checkpoint != null ? JSON.stringify(state.checkpoint) : null,
|
|
69
|
+
last_synced_at: state.lastSyncedAt,
|
|
70
|
+
entity_count: state.entityCount,
|
|
71
|
+
status: state.status,
|
|
72
|
+
error: state.error ?? null
|
|
73
|
+
})).execute();
|
|
74
|
+
},
|
|
75
|
+
async claimSync (connectorName, ownerDID, leaseMs) {
|
|
76
|
+
const now = Date.now();
|
|
77
|
+
const leaseExpiresAt = now + leaseMs;
|
|
78
|
+
// Single atomic upsert. INSERT wins when no row exists. On conflict the
|
|
79
|
+
// DO UPDATE only fires when its WHERE holds: the existing lease is not
|
|
80
|
+
// 'syncing', is unset, or has expired. The guard columns are
|
|
81
|
+
// table-qualified because a bare 'status'/'lease_expires_at' is ambiguous
|
|
82
|
+
// on Postgres (both the target row and the excluded row are in scope).
|
|
83
|
+
// A live lease fails the WHERE, so no row is returned.
|
|
84
|
+
const result = await db.insertInto('kubun_connector_sync_state').values({
|
|
85
|
+
connector_name: connectorName,
|
|
86
|
+
owner_did: ownerDID,
|
|
87
|
+
checkpoint: null,
|
|
88
|
+
last_synced_at: new Date(now).toISOString(),
|
|
89
|
+
entity_count: 0,
|
|
90
|
+
status: 'syncing',
|
|
91
|
+
error: null,
|
|
92
|
+
lease_expires_at: leaseExpiresAt
|
|
93
|
+
}).onConflict((oc)=>oc.columns([
|
|
94
|
+
'connector_name',
|
|
95
|
+
'owner_did'
|
|
96
|
+
]).doUpdateSet({
|
|
97
|
+
status: 'syncing',
|
|
98
|
+
lease_expires_at: leaseExpiresAt
|
|
99
|
+
}).where((eb)=>eb.or([
|
|
100
|
+
eb('kubun_connector_sync_state.status', '!=', 'syncing'),
|
|
101
|
+
eb('kubun_connector_sync_state.lease_expires_at', 'is', null),
|
|
102
|
+
eb('kubun_connector_sync_state.lease_expires_at', '<', now)
|
|
103
|
+
]))).returningAll().executeTakeFirst();
|
|
104
|
+
return result != null;
|
|
105
|
+
},
|
|
106
|
+
async renewSync (connectorName, ownerDID, leaseMs) {
|
|
107
|
+
const now = Date.now();
|
|
108
|
+
// Extend the lease only while the row is still actively syncing. A plain
|
|
109
|
+
// UPDATE (no excluded row in scope) needs no table-qualified columns.
|
|
110
|
+
await db.updateTable('kubun_connector_sync_state').set({
|
|
111
|
+
lease_expires_at: now + leaseMs
|
|
112
|
+
}).where('connector_name', '=', connectorName).where('owner_did', '=', ownerDID).where('status', '=', 'syncing').execute();
|
|
113
|
+
},
|
|
114
|
+
async deleteSyncState (connectorName, ownerDID) {
|
|
115
|
+
await db.deleteFrom('kubun_connector_sync_state').where('connector_name', '=', connectorName).where('owner_did', '=', ownerDID).execute();
|
|
116
|
+
},
|
|
117
|
+
async listSyncStatesForConnector (connectorName) {
|
|
118
|
+
const rows = await db.selectFrom('kubun_connector_sync_state').selectAll().where('connector_name', '=', connectorName).execute();
|
|
119
|
+
return rows.map((row)=>({
|
|
120
|
+
...rowToSyncState(row),
|
|
121
|
+
connectorName: row.connector_name,
|
|
122
|
+
ownerDID: row.owner_did
|
|
123
|
+
}));
|
|
124
|
+
},
|
|
125
|
+
// --- Pending OAuth authorization operations ---
|
|
126
|
+
async createPendingAuth (record) {
|
|
127
|
+
await db.insertInto('kubun_connector_pending_auth').values({
|
|
128
|
+
state: record.state,
|
|
129
|
+
code_verifier: record.codeVerifier,
|
|
130
|
+
provider: record.provider,
|
|
131
|
+
owner_did: record.ownerDID,
|
|
132
|
+
created_at: record.createdAt
|
|
133
|
+
}).execute();
|
|
134
|
+
},
|
|
135
|
+
async consumePendingAuth (state) {
|
|
136
|
+
// Atomic single-use read: DELETE ... RETURNING removes and returns the row
|
|
137
|
+
// in one statement so concurrent callbacks cannot both consume the state.
|
|
138
|
+
const row = await db.deleteFrom('kubun_connector_pending_auth').where('state', '=', state).returningAll().executeTakeFirst();
|
|
139
|
+
if (row == null) return null;
|
|
140
|
+
return {
|
|
141
|
+
state: row.state,
|
|
142
|
+
codeVerifier: row.code_verifier,
|
|
143
|
+
provider: row.provider,
|
|
144
|
+
ownerDID: row.owner_did,
|
|
145
|
+
createdAt: row.created_at
|
|
146
|
+
};
|
|
147
|
+
},
|
|
148
|
+
async deleteExpiredPendingAuth (cutoffMs) {
|
|
149
|
+
await db.deleteFrom('kubun_connector_pending_auth').where('created_at', '<', cutoffMs).execute();
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
}
|
package/lib/definition.js
CHANGED
|
@@ -1 +1,7 @@
|
|
|
1
|
-
import{createConnectorStore
|
|
1
|
+
import { createConnectorStore } from './api.js';
|
|
2
|
+
import { getConnectorMigrations } from './migrations.js';
|
|
3
|
+
export const connectorStoreDefinition = {
|
|
4
|
+
name: 'connector',
|
|
5
|
+
migrations: getConnectorMigrations,
|
|
6
|
+
createAPI: createConnectorStore
|
|
7
|
+
};
|
package/lib/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { StoreProvider } from '@kubun/db';
|
|
2
2
|
import type { ConnectorStoreAPI } from './api.js';
|
|
3
|
-
export type { ConnectorStoreAPI,
|
|
3
|
+
export type { ConnectorStoreAPI, PendingAuthRecord, SyncState, SyncStateEntry, SyncStatus, } from './api.js';
|
|
4
4
|
export { createConnectorStore } from './api.js';
|
|
5
5
|
export { connectorStoreDefinition } from './definition.js';
|
|
6
6
|
export declare const CONNECTOR_STORE: "connector";
|
package/lib/index.js
CHANGED
|
@@ -1 +1,6 @@
|
|
|
1
|
-
export{createConnectorStore}from
|
|
1
|
+
export { createConnectorStore } from './api.js';
|
|
2
|
+
export { connectorStoreDefinition } from './definition.js';
|
|
3
|
+
export const CONNECTOR_STORE = 'connector';
|
|
4
|
+
export function getConnectorStore(provider) {
|
|
5
|
+
return provider.getStore(CONNECTOR_STORE);
|
|
6
|
+
}
|
package/lib/migrations.d.ts
CHANGED
package/lib/migrations.js
CHANGED
|
@@ -1 +1,34 @@
|
|
|
1
|
-
export function getConnectorMigrations(
|
|
1
|
+
export function getConnectorMigrations(ctx) {
|
|
2
|
+
const t = ctx.types;
|
|
3
|
+
const init = {
|
|
4
|
+
async up (db) {
|
|
5
|
+
// Connector sync state
|
|
6
|
+
await db.schema.createTable('kubun_connector_sync_state').ifNotExists().addColumn('connector_name', t.text, (col)=>col.notNull()).addColumn('owner_did', t.text, (col)=>col.notNull()).addColumn('checkpoint', t.text).addColumn('last_synced_at', t.text, (col)=>col.notNull()).addColumn('entity_count', 'integer', (col)=>col.notNull().defaultTo(0)).addColumn('status', t.text, (col)=>col.notNull()).addColumn('error', t.text)// Lease expiry for atomic sync-claim coordination. ms-epoch (Date.now)
|
|
7
|
+
// overflows int32 on Postgres; bigint holds it and the adapter's int8
|
|
8
|
+
// parser reads it back as a JS number. Nullable: a row with no active
|
|
9
|
+
// lease leaves this null.
|
|
10
|
+
.addColumn('lease_expires_at', t.bigint).addPrimaryKeyConstraint('kubun_connector_sync_state_pkey', [
|
|
11
|
+
'connector_name',
|
|
12
|
+
'owner_did'
|
|
13
|
+
]).execute();
|
|
14
|
+
// Connector credentials
|
|
15
|
+
await db.schema.createTable('kubun_connector_credentials').ifNotExists().addColumn('provider_name', t.text, (col)=>col.notNull()).addColumn('owner_did', t.text, (col)=>col.notNull()).addColumn('credential', t.text, (col)=>col.notNull()).addPrimaryKeyConstraint('kubun_connector_credentials_pkey', [
|
|
16
|
+
'provider_name',
|
|
17
|
+
'owner_did'
|
|
18
|
+
]).execute();
|
|
19
|
+
// Pending OAuth authorizations: state + PKCE verifier persisted between the
|
|
20
|
+
// authorize redirect and the callback. State is single-use, consumed on read.
|
|
21
|
+
await db.schema.createTable('kubun_connector_pending_auth').ifNotExists().addColumn('state', t.text, (col)=>col.notNull().primaryKey()).addColumn('code_verifier', t.text, (col)=>col.notNull()).addColumn('provider', t.text, (col)=>col.notNull()).addColumn('owner_did', t.text, (col)=>col.notNull())// ms-epoch (Date.now) overflows int32 on Postgres; bigint holds it and
|
|
22
|
+
// the adapter's int8 parser reads it back as a JS number.
|
|
23
|
+
.addColumn('created_at', t.bigint, (col)=>col.notNull()).execute();
|
|
24
|
+
},
|
|
25
|
+
async down (db) {
|
|
26
|
+
await db.schema.dropTable('kubun_connector_pending_auth').ifExists().execute();
|
|
27
|
+
await db.schema.dropTable('kubun_connector_credentials').ifExists().execute();
|
|
28
|
+
await db.schema.dropTable('kubun_connector_sync_state').ifExists().execute();
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
return {
|
|
32
|
+
'0-init': init
|
|
33
|
+
};
|
|
34
|
+
}
|
package/lib/tables.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export type ConnectorSyncStateTable = {
|
|
|
7
7
|
entity_count: number;
|
|
8
8
|
status: string;
|
|
9
9
|
error: string | null;
|
|
10
|
+
lease_expires_at: number | null;
|
|
10
11
|
};
|
|
11
12
|
export type ConnectorSyncState = Selectable<ConnectorSyncStateTable>;
|
|
12
13
|
export type ConnectorCredentialTable = {
|
|
@@ -15,7 +16,16 @@ export type ConnectorCredentialTable = {
|
|
|
15
16
|
credential: string;
|
|
16
17
|
};
|
|
17
18
|
export type ConnectorCredential = Selectable<ConnectorCredentialTable>;
|
|
19
|
+
export type ConnectorPendingAuthTable = {
|
|
20
|
+
state: string;
|
|
21
|
+
code_verifier: string;
|
|
22
|
+
provider: string;
|
|
23
|
+
owner_did: string;
|
|
24
|
+
created_at: number;
|
|
25
|
+
};
|
|
26
|
+
export type ConnectorPendingAuth = Selectable<ConnectorPendingAuthTable>;
|
|
18
27
|
export type ConnectorTables = {
|
|
19
28
|
kubun_connector_sync_state: ConnectorSyncStateTable;
|
|
20
29
|
kubun_connector_credentials: ConnectorCredentialTable;
|
|
30
|
+
kubun_connector_pending_auth: ConnectorPendingAuthTable;
|
|
21
31
|
};
|
package/lib/tables.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
// --- Aggregate ConnectorTables type ---
|
|
2
|
+
export { };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubun/store-connector",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"license": "see LICENSE.md",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"type": "module",
|
|
@@ -15,16 +15,18 @@
|
|
|
15
15
|
],
|
|
16
16
|
"sideEffects": false,
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"kysely": "^0.
|
|
19
|
-
"@kubun/db": "^0.
|
|
20
|
-
"@kubun/db
|
|
18
|
+
"kysely": "^0.29.2",
|
|
19
|
+
"@kubun/db-adapter": "^0.11.0",
|
|
20
|
+
"@kubun/db": "^0.11.0"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@
|
|
23
|
+
"@testcontainers/postgresql": "^12.0.4",
|
|
24
|
+
"@kubun/db-postgres": "^0.11.0",
|
|
25
|
+
"@kubun/db-better-sqlite": "^0.11.0"
|
|
24
26
|
},
|
|
25
27
|
"scripts": {
|
|
26
28
|
"build:clean": "del lib",
|
|
27
|
-
"build:js": "swc src -d ./lib --config-file ../../swc.json --strip-leading-paths",
|
|
29
|
+
"build:js": "swc src -d ./lib --config-file ../../node_modules/@kigu/dev/swc.json --strip-leading-paths",
|
|
28
30
|
"build:types": "tsc --emitDeclarationOnly --skipLibCheck",
|
|
29
31
|
"build:types:ci": "tsc --emitDeclarationOnly --declarationMap false",
|
|
30
32
|
"build": "pnpm run build:clean && pnpm run build:js && pnpm run build:types",
|