@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 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<Credential | null>;
26
- setCredential(providerName: string, ownerDID: string, credential: Credential | string): Promise<void>;
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
- function e(e){let t=null;null!=e.checkpoint&&(t="string"==typeof e.checkpoint?JSON.parse(e.checkpoint):e.checkpoint);let n={checkpoint:t,lastSyncedAt:e.last_synced_at,entityCount:e.entity_count,status:e.status};return null!=e.error&&(n.error=e.error),n}export function createConnectorStore(t,n){return{async getCredential(e,n){var c;let r,o,a=await t.selectFrom("kubun_connector_credentials").select("credential").where("provider_name","=",e).where("owner_did","=",n).executeTakeFirst();return null==a?null:(o={accessToken:(r="string"==typeof(c=a.credential)?JSON.parse(c):c).accessToken,scopes:r.scopes},null!=r.refreshToken&&(o.refreshToken=r.refreshToken),null!=r.expiresAt&&(o.expiresAt=new Date(r.expiresAt)),null!=r.accountLabel&&(o.accountLabel=r.accountLabel),null!=r.metadata&&(o.metadata=r.metadata),o)},async setCredential(e,n,c){let r,o="string"==typeof c?c:(r={accessToken:c.accessToken,scopes:c.scopes},null!=c.refreshToken&&(r.refreshToken=c.refreshToken),null!=c.expiresAt&&(r.expiresAt=c.expiresAt.toISOString()),null!=c.accountLabel&&(r.accountLabel=c.accountLabel),null!=c.metadata&&(r.metadata=c.metadata),JSON.stringify(r));await t.insertInto("kubun_connector_credentials").values({provider_name:e,owner_did:n,credential:o}).onConflict(e=>e.columns(["provider_name","owner_did"]).doUpdateSet({credential:o})).execute()},async deleteCredential(e,n){await t.deleteFrom("kubun_connector_credentials").where("provider_name","=",e).where("owner_did","=",n).execute()},async getSyncState(n,c){let r=await t.selectFrom("kubun_connector_sync_state").selectAll().where("connector_name","=",n).where("owner_did","=",c).executeTakeFirst();return null==r?null:e(r)},async setSyncState(e,n,c){await t.insertInto("kubun_connector_sync_state").values({connector_name:e,owner_did:n,checkpoint:null!=c.checkpoint?JSON.stringify(c.checkpoint):null,last_synced_at:c.lastSyncedAt,entity_count:c.entityCount,status:c.status,error:c.error??null}).onConflict(e=>e.columns(["connector_name","owner_did"]).doUpdateSet({checkpoint:null!=c.checkpoint?JSON.stringify(c.checkpoint):null,last_synced_at:c.lastSyncedAt,entity_count:c.entityCount,status:c.status,error:c.error??null})).execute()},async deleteSyncState(e,n){await t.deleteFrom("kubun_connector_sync_state").where("connector_name","=",e).where("owner_did","=",n).execute()},listSyncStatesForConnector:async n=>(await t.selectFrom("kubun_connector_sync_state").selectAll().where("connector_name","=",n).execute()).map(t=>({...e(t),connectorName:t.connector_name,ownerDID:t.owner_did}))}}
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 as o}from"./api.js";import{getConnectorMigrations as r}from"./migrations.js";export const connectorStoreDefinition={name:"connector",migrations:r,createAPI:o};
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, Credential, SyncState, SyncStateEntry, SyncStatus } from './api.js';
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"./api.js";export{connectorStoreDefinition}from"./definition.js";export const CONNECTOR_STORE="connector";export function getConnectorStore(o){return o.getStore("connector")}
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
+ }
@@ -1,3 +1,3 @@
1
1
  import type { MigrationContext } from '@kubun/db';
2
- import type { Migration } from 'kysely';
2
+ import type { Migration } from 'kysely/migration';
3
3
  export declare function getConnectorMigrations(ctx: MigrationContext): Record<string, Migration>;
package/lib/migrations.js CHANGED
@@ -1 +1,34 @@
1
- export function getConnectorMigrations(t){let e=t.types;return{"0-init":{async up(t){await t.schema.createTable("kubun_connector_sync_state").ifNotExists().addColumn("connector_name",e.text,t=>t.notNull()).addColumn("owner_did",e.text,t=>t.notNull()).addColumn("checkpoint",e.text).addColumn("last_synced_at",e.text,t=>t.notNull()).addColumn("entity_count","integer",t=>t.notNull().defaultTo(0)).addColumn("status",e.text,t=>t.notNull()).addColumn("error",e.text).addPrimaryKeyConstraint("kubun_connector_sync_state_pkey",["connector_name","owner_did"]).execute(),await t.schema.createTable("kubun_connector_credentials").ifNotExists().addColumn("provider_name",e.text,t=>t.notNull()).addColumn("owner_did",e.text,t=>t.notNull()).addColumn("credential",e.text,t=>t.notNull()).addPrimaryKeyConstraint("kubun_connector_credentials_pkey",["provider_name","owner_did"]).execute()},async down(t){await t.schema.dropTable("kubun_connector_credentials").ifExists().execute(),await t.schema.dropTable("kubun_connector_sync_state").ifExists().execute()}}}}
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
- export{};
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.9.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.28.17",
19
- "@kubun/db": "^0.9.0",
20
- "@kubun/db-adapter": "^0.9.0"
18
+ "kysely": "^0.29.2",
19
+ "@kubun/db-adapter": "^0.11.0",
20
+ "@kubun/db": "^0.11.0"
21
21
  },
22
22
  "devDependencies": {
23
- "@kubun/db-better-sqlite": "^0.9.0"
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",