@kubun/plugin-connector 0.8.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/LICENSE.md ADDED
@@ -0,0 +1,57 @@
1
+ # The Prosperity Public License 3.0.0
2
+
3
+ Contributor: Paul Le Cam
4
+
5
+ Source Code: https://github.com/PaulLeCam/kubun
6
+
7
+ ## Purpose
8
+
9
+ This license allows you to use and share this software for noncommercial purposes for free and to try this software for commercial purposes for thirty days.
10
+
11
+ ## Agreement
12
+
13
+ In order to receive this license, you have to agree to its rules. Those rules are both obligations under that agreement and conditions to your license. Don't do anything with this software that triggers a rule you can't or won't follow.
14
+
15
+ ## Notices
16
+
17
+ Make sure everyone who gets a copy of any part of this software from you, with or without changes, also gets the text of this license and the contributor and source code lines above.
18
+
19
+ ## Commercial Trial
20
+
21
+ Limit your use of this software for commercial purposes to a thirty-day trial period. If you use this software for work, your company gets one trial period for all personnel, not one trial per person.
22
+
23
+ ## Contributions Back
24
+
25
+ Developing feedback, changes, or additions that you contribute back to the contributor on the terms of a standardized public software license such as [the Blue Oak Model License 1.0.0](https://blueoakcouncil.org/license/1.0.0), [the Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0.html), [the MIT license](https://spdx.org/licenses/MIT.html), or [the two-clause BSD license](https://spdx.org/licenses/BSD-2-Clause.html) doesn't count as use for a commercial purpose.
26
+
27
+ ## Personal Uses
28
+
29
+ Personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious observance, without any anticipated commercial application, doesn't count as use for a commercial purpose.
30
+
31
+ ## Noncommercial Organizations
32
+
33
+ Use by any charitable organization, educational institution, public research organization, public safety or health organization, environmental protection organization, or government institution doesn't count as use for a commercial purpose regardless of the source of funding or obligations resulting from the funding.
34
+
35
+ ## Defense
36
+
37
+ Don't make any legal claim against anyone accusing this software, with or without changes, alone or with other technology, of infringing any patent.
38
+
39
+ ## Copyright
40
+
41
+ The contributor licenses you to do everything with this software that would otherwise infringe their copyright in it.
42
+
43
+ ## Patent
44
+
45
+ The contributor licenses you to do everything with this software that would otherwise infringe any patents they can license or become able to license.
46
+
47
+ ## Reliability
48
+
49
+ The contributor can't revoke this license.
50
+
51
+ ## Excuse
52
+
53
+ You're excused for unknowingly breaking [Notices](#notices) if you take all practical steps to comply within thirty days of learning you broke the rule.
54
+
55
+ ## No Liability
56
+
57
+ ***As far as the law allows, this software comes as is, without any warranty or condition, and the contributor won't be liable to anyone for any damages related to this software or this license, under any kind of legal claim.***
@@ -0,0 +1,21 @@
1
+ import type { CredentialProvider, EntityRecord } from '@kubun/connector';
2
+ import type { KubunDB, WritableDB } from '@kubun/db';
3
+ import type { ConnectorRegistry } from './registry.js';
4
+ export type ExecuteActionParams = {
5
+ connector: string;
6
+ action: string;
7
+ model: string;
8
+ input: Record<string, unknown>;
9
+ sourceID?: string;
10
+ };
11
+ export type ExecuteActionResult = {
12
+ entity: EntityRecord;
13
+ documentID: string | null;
14
+ };
15
+ export type ExecuteActionDeps = {
16
+ db: KubunDB | WritableDB;
17
+ registry: ConnectorRegistry;
18
+ credentialProvider: CredentialProvider;
19
+ ownerDID: string;
20
+ };
21
+ export declare function executeAction(params: ExecuteActionParams, deps: ExecuteActionDeps): Promise<ExecuteActionResult>;
package/lib/action.js ADDED
@@ -0,0 +1 @@
1
+ import{DocumentID as e,DocumentModelID as r}from"@kubun/id";import{EntityProcessor as o}from"./sync/processor.js";export async function executeAction(t,n){let l,c,{db:i,registry:a,credentialProvider:u,ownerDID:s}=n,{connector:d,action:f,model:p,input:w,sourceID:m}=t,E=a.get(d);if(null==E)throw Error(`Connector "${d}" not found`);if(null==E.actionHandler)throw Error(`Connector "${d}" does not support actions`);if(null==E.actions?.find(e=>e.name===f))throw Error(`Action "${f}" not found on connector "${d}"`);let h=E.auth.provider,S=await u.get(h,s);if(null==S)throw Error(`No credential found for provider "${h}"`);let $=E.auth;if(null!=$.writeScopes&&!$.writeScopes.every(e=>S.scopes.includes(e))){let e=Error("INSUFFICIENT_SCOPES");throw e.code="INSUFFICIENT_SCOPES",e.requiredScopes=$.writeScopes,e}for(let e of Object.values(E.clusters)){for(let[r,o]of Object.entries(e.record))if(e.models[o].name===p){l=r;break}if(null!=l)break}if(null==l)throw Error(`Model "${p}" not found in connector "${d}" clusters`);let I=w;if("update"===f&&null!=m){let o=new TextEncoder().encode(`${d}:${m}`),t=e.create(r.fromString(l),s,o),n=await i.getDocument(t);n?.data!=null&&(I={...n.data,...w})}let b=E.actionHandler({credential:S});if("create"===f)c=await b.create(p,I);else if("update"===f){if(null==m)throw Error("sourceID is required for update actions");c=await b.update(p,m,I)}else throw Error(`Unsupported action: ${f}`);let C={};for(let e of Object.values(E.clusters))if(null!=e.record[l]){C=e.models[e.record[l]]?.fieldsMeta??{};break}let v=new o({db:i,modelID:l,ownerDID:s,connectorName:d,edgeFields:C,clusters:E.clusters}),N=await v.processBatch({entities:[c]});return{entity:c,documentID:N.documentIDs[0]??null}}
package/lib/api.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ import type { KubunDB } from '@kubun/db';
2
+ export type SyncStateData = {
3
+ connectorName: string;
4
+ ownerDID: string;
5
+ checkpoint: string | null;
6
+ lastSyncedAt: string;
7
+ entityCount: number;
8
+ status: string;
9
+ error: string | null;
10
+ };
11
+ export type SetSyncStateParams = {
12
+ checkpoint?: string | null;
13
+ lastSyncedAt?: string;
14
+ entityCount?: number;
15
+ status: string;
16
+ error?: string | null;
17
+ };
18
+ export type ConnectorAPI = {
19
+ getSyncState: (connectorName: string, ownerDID: string) => Promise<SyncStateData | null>;
20
+ setSyncState: (connectorName: string, ownerDID: string, state: SetSyncStateParams) => Promise<SyncStateData>;
21
+ deleteSyncState: (connectorName: string, ownerDID: string) => Promise<void>;
22
+ listSyncStates: (connectorName: string) => Promise<Array<SyncStateData>>;
23
+ getCredential: (providerName: string, ownerDID: string) => Promise<string | null>;
24
+ setCredential: (providerName: string, ownerDID: string, credential: string) => Promise<void>;
25
+ deleteCredential: (providerName: string, ownerDID: string) => Promise<void>;
26
+ };
27
+ export declare function createConnectorAPI(params: {
28
+ db: KubunDB;
29
+ }): ConnectorAPI;
package/lib/api.js ADDED
@@ -0,0 +1 @@
1
+ function e(e){return{connectorName:e.connector_name,ownerDID:e.owner_did,checkpoint:e.checkpoint,lastSyncedAt:e.last_synced_at,entityCount:e.entity_count,status:e.status,error:e.error}}export function createConnectorAPI(t){return{async getSyncState(n,r){let a=await t.db.getDB(),c=await a.selectFrom("kubun_connector_sync_state").selectAll().where("connector_name","=",n).where("owner_did","=",r).executeTakeFirst();return null!=c?e(c):null},async setSyncState(n,r,a){let c=await t.db.getDB(),o=a.lastSyncedAt??new Date().toISOString(),s=a.entityCount??0,l=a.checkpoint??null,i=a.error??null;await c.insertInto("kubun_connector_sync_state").values({connector_name:n,owner_did:r,checkpoint:l,last_synced_at:o,entity_count:s,status:a.status,error:i}).onConflict(e=>e.columns(["connector_name","owner_did"]).doUpdateSet({checkpoint:l,last_synced_at:o,entity_count:s,status:a.status,error:i})).execute();let u=await c.selectFrom("kubun_connector_sync_state").selectAll().where("connector_name","=",n).where("owner_did","=",r).executeTakeFirst();if(null==u)throw Error("Failed to set sync state");return e(u)},async deleteSyncState(e,n){let r=await t.db.getDB();await r.deleteFrom("kubun_connector_sync_state").where("connector_name","=",e).where("owner_did","=",n).execute()},async listSyncStates(n){let r=await t.db.getDB();return(await r.selectFrom("kubun_connector_sync_state").selectAll().where("connector_name","=",n).execute()).map(e)},async getCredential(e,n){let r=await t.db.getDB(),a=await r.selectFrom("kubun_connector_credentials").selectAll().where("provider_name","=",e).where("owner_did","=",n).executeTakeFirst();return null!=a?a.credential:null},async setCredential(e,n,r){let a=await t.db.getDB();await a.insertInto("kubun_connector_credentials").values({provider_name:e,owner_did:n,credential:r}).onConflict(e=>e.columns(["provider_name","owner_did"]).doUpdateSet({credential:r})).execute()},async deleteCredential(e,n){let r=await t.db.getDB();await r.deleteFrom("kubun_connector_credentials").where("provider_name","=",e).where("owner_did","=",n).execute()}}}
@@ -0,0 +1,4 @@
1
+ import type { SyncBoundary } from '@kubun/connector';
2
+ export declare function parseDuration(duration: string): number | undefined;
3
+ export declare function computeEffectiveBoundary(serverBoundary: SyncBoundary, userBoundary: SyncBoundary | undefined): SyncBoundary;
4
+ export declare function isWithinBoundary(date: Date, boundary: SyncBoundary): boolean;
@@ -0,0 +1 @@
1
+ let e=/^P(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/;export function parseDuration(n){let i=e.exec(n);if(!i)return;let t=Number.parseInt(i[1]??"0",10),l=Number.parseInt(i[2]??"0",10),a=Number.parseInt(i[3]??"0",10),u=Number.parseInt(i[4]??"0",10);if(0!==t||0!==l||0!==a||0!==u)return((24*t+l)*60+a)*6e4+1e3*u}export function computeEffectiveBoundary(e,n){if(null==n)return e;let i={};if(null!=e.maxAge||null!=n.maxAge){let t=e.maxAge?parseDuration(e.maxAge):void 0,l=n.maxAge?parseDuration(n.maxAge):void 0;null!=t&&null!=l?i.maxAge=t<=l?e.maxAge:n.maxAge:i.maxAge=e.maxAge??n.maxAge}if(null!=e.since||null!=n.since){let t=e.since?new Date(e.since):void 0,l=n.since?new Date(n.since):void 0;null!=t&&null!=l?i.since=t>=l?e.since:n.since:i.since=e.since??n.since}if(null!=e.maxEntities||null!=n.maxEntities){let t=e.maxEntities,l=n.maxEntities;null!=t&&null!=l?i.maxEntities=Math.min(t,l):i.maxEntities=e.maxEntities??n.maxEntities}return i}export function isWithinBoundary(e,n){let i=Date.now();if(null!=n.maxAge){let t=parseDuration(n.maxAge);if(null!=t&&e.getTime()<i-t)return!1}if(null!=n.since){let i=new Date(n.since);if(e.getTime()<i.getTime())return!1}return!0}
@@ -0,0 +1,16 @@
1
+ import type { Runtime } from '@enkaku/runtime';
2
+ import type { Credential, CredentialProvider, OAuthProviderDefinition } from '@kubun/connector';
3
+ import type { ConnectorAPI } from './api.js';
4
+ export type DBCredentialProviderParams = {
5
+ api: ConnectorAPI;
6
+ runtime: Runtime;
7
+ providers: Array<OAuthProviderDefinition>;
8
+ bufferSeconds?: number;
9
+ };
10
+ export declare class DBCredentialProvider implements CredentialProvider {
11
+ #private;
12
+ constructor(params: DBCredentialProviderParams);
13
+ get(providerName: string, ownerDID: string): Promise<Credential | null>;
14
+ set(providerName: string, ownerDID: string, credential: Credential): Promise<void>;
15
+ delete(providerName: string, ownerDID: string): Promise<void>;
16
+ }
@@ -0,0 +1 @@
1
+ export class DBCredentialProvider{#e;#t;#n;#r;constructor(e){this.#e=e.api,this.#t=e.runtime,this.#n=e.providers,this.#r=(e.bufferSeconds??300)*1e3}async get(e,t){let n,r,s=await this.#e.getCredential(e,t);if(null==s)return null;let a=(r={accessToken:(n="string"==typeof s?JSON.parse(s):s).accessToken,scopes:n.scopes},null!=n.refreshToken&&(r.refreshToken=n.refreshToken),null!=n.expiresAt&&(r.expiresAt=new Date(n.expiresAt)),null!=n.accountLabel&&(r.accountLabel=n.accountLabel),null!=n.metadata&&(r.metadata=n.metadata),r);return this.#s(a)&&null!=a.refreshToken?await this.#a(e,t,a,a.refreshToken)??a:a}async set(e,t,n){let r;await this.#e.setCredential(e,t,(r={accessToken:n.accessToken,scopes:n.scopes},null!=n.refreshToken&&(r.refreshToken=n.refreshToken),null!=n.expiresAt&&(r.expiresAt=n.expiresAt.toISOString()),null!=n.accountLabel&&(r.accountLabel=n.accountLabel),null!=n.metadata&&(r.metadata=n.metadata),JSON.stringify(r)))}async delete(e,t){await this.#e.deleteCredential(e,t)}#s(e){return null!=e.expiresAt&&e.expiresAt.getTime()-Date.now()<this.#r}async #a(e,t,n,r){let s=this.#n.find(t=>t.name===e);if(null==s||null==s.clientID||null==s.clientSecret)return null;try{let a=await this.#t.fetch(s.tokenEndpoint,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({grant_type:"refresh_token",refresh_token:r,client_id:s.clientID,client_secret:s.clientSecret})});if(!a.ok)return null;let i=await a.json(),l={accessToken:i.access_token,refreshToken:i.refresh_token??n.refreshToken,expiresAt:null!=i.expires_in?new Date(Date.now()+1e3*i.expires_in):void 0,scopes:i.scope?.split(" ")??n.scopes,accountLabel:n.accountLabel,metadata:n.metadata};return await this.set(e,t,l),l}catch{return null}}}
package/lib/index.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ import type { ConnectorDefinition, OAuthProviderDefinition, SyncBoundary } from '@kubun/connector';
2
+ import type { KubunPlugin, PluginFactoryParams } from '@kubun/engine';
3
+ import { type ConnectorAPI, type SetSyncStateParams, type SyncStateData } from './api.js';
4
+ import { DBCredentialProvider, type DBCredentialProviderParams } from './credential.js';
5
+ export { type ExecuteActionDeps, type ExecuteActionParams, type ExecuteActionResult, executeAction, } from './action.js';
6
+ export { type ConnectorActivatedEvent, type ConnectorDeactivatedEvent, ConnectorManager, type ConnectorManagerEvents, type ConnectorManagerParams, } from './manager.js';
7
+ export { OAuthService, type OAuthServiceParams } from './oauth.js';
8
+ export { ConnectorRegistry } from './registry.js';
9
+ export type { CompleteConnectorAuthInput, CompleteConnectorAuthOutput, ConnectorActionError, ConnectorActionInput, ConnectorActionResult, ConnectorExtensionConfig, ConnectorQueryContext, ConnectorState, ConnectorSyncEventResult, StartConnectorAuthInput, StartConnectorAuthOutput, SyncTriggerInput, SyncTriggerOutput, } from './schema.js';
10
+ export { createConnectorSchemaExtension, toConnectorSyncEventResult } from './schema.js';
11
+ export { type RunSyncParams, SyncEngine, type SyncEngineParams } from './sync/engine.js';
12
+ export { type OrchestrateSyncParams, type OrchestrateSyncResult, orchestrateSync, type SyncEventEmitter, } from './sync/orchestrate.js';
13
+ export { EntityProcessor, type EntityProcessorParams, type ProcessBatchResult, } from './sync/processor.js';
14
+ export { DBSyncStateStore } from './sync/state.js';
15
+ export type { ConnectorAPI, SetSyncStateParams, SyncStateData };
16
+ export { DBCredentialProvider, type DBCredentialProviderParams };
17
+ export type ConnectorPluginOptions = {
18
+ connectors: Array<ConnectorDefinition>;
19
+ providers?: Array<OAuthProviderDefinition>;
20
+ defaults?: {
21
+ boundary?: SyncBoundary;
22
+ pollingInterval?: string;
23
+ };
24
+ };
25
+ export declare function createConnectorPlugin(options: ConnectorPluginOptions): (params: PluginFactoryParams) => KubunPlugin;
package/lib/index.js ADDED
@@ -0,0 +1 @@
1
+ import{fromEmitter as e}from"@enkaku/generator";import{executeAction as t}from"./action.js";import{createConnectorAPI as r}from"./api.js";import{DBCredentialProvider as n}from"./credential.js";import{ConnectorManager as o}from"./manager.js";import{createConnectorMigrations as c}from"./migration.js";import{OAuthService as s}from"./oauth.js";import{ConnectorRegistry as a}from"./registry.js";import{createConnectorSchemaExtension as i,toConnectorSyncEventResult as u}from"./schema.js";import{orchestrateSync as l}from"./sync/orchestrate.js";import{DBSyncStateStore as m}from"./sync/state.js";export{executeAction}from"./action.js";export{ConnectorManager}from"./manager.js";export{OAuthService}from"./oauth.js";export{ConnectorRegistry}from"./registry.js";export{createConnectorSchemaExtension,toConnectorSyncEventResult}from"./schema.js";export{SyncEngine}from"./sync/engine.js";export{orchestrateSync}from"./sync/orchestrate.js";export{EntityProcessor}from"./sync/processor.js";export{DBSyncStateStore}from"./sync/state.js";async function p(e,t,r,n,o){let c=t.get(o);if(null==c)throw Error(`Connector "${o}" not found`);let s=await e.getSyncState(o,n),a=!1,i=!1,u=null;if("device"!==c.auth.provider){let e=await r.get(c.auth.provider,n);if(null!=e){a=!0,null!=e.expiresAt&&(u=e.expiresAt.toISOString());let t=c.auth;i=null==t.writeScopes||t.writeScopes.every(t=>e.scopes.includes(t))}}return null==s?{name:o,status:"IDLE",authenticated:a,hasWriteAccess:i,authExpiresAt:u,lastSyncedAt:null,entityCount:null,error:null}:{name:o,status:"idle"===s.status?"IDLE":"syncing"===s.status?"SYNCING":"ERROR",authenticated:a,hasWriteAccess:i,authExpiresAt:u,lastSyncedAt:s.lastSyncedAt??null,entityCount:s.entityCount??null,error:s.error??null}}export function createConnectorPlugin(y){let f=y.providers??[];return d=>{let g=new a;for(let e of y.connectors)g.register(e);let S=r({db:d.db}),D=new n({api:S,runtime:d.runtime,providers:f}),h=new m({api:S}),w=new o({registry:g,logger:d.getLogger("connector"),defaults:y.defaults}),C=new s({runtime:d.runtime,providers:f,registry:g,credentialProvider:D}),j={types:d.db.adapter.types,functions:d.db.adapter.functions};return{name:"connector",migrations:{name:"connector",migrations:()=>c(j)},schemaExtension:e=>i({registry:g,config:e}),api:S,createContextFactory:()=>r=>({getConnectorState:e=>p(S,g,D,r.viewerDID,e),getConnectorStates:()=>Promise.all(g.list().map(e=>p(S,g,D,r.viewerDID,e))),startConnectorAuth:e=>C.startAuth(e),completeConnectorAuth:e=>C.completeAuth(e,r.viewerDID),triggerConnectorSync:async e=>{if(!g.has(e.connector))return{status:"ERROR",connectorName:e.connector};let t=await l({connectorName:e.connector,full:e.full??!1,registry:g,syncEventEmitter:w,stateStore:h,credentialProvider:D,ownerDID:r.viewerDID,db:d.db,boundary:y.defaults?.boundary});return{status:"started"===t.status?"STARTED":"already_syncing"===t.status?"ALREADY_SYNCING":"ERROR",connectorName:e.connector}},subscribeToConnectorSyncEvents:t=>{let r=null!=t?e=>e.connectorName===t:void 0,n=e(w.syncEvents,"sync",{filter:r});return async function*(){for await(let e of n)yield u(e)}()},executeConnectorAction:async e=>{try{let n=await t({connector:e.connector,action:e.action,model:e.model,input:e.input,sourceID:e.sourceID},{db:d.db,registry:g,credentialProvider:D,ownerDID:r.viewerDID});return{documentID:n.documentID,entity:n.entity,error:null}}catch(t){let e=t.code??"API_ERROR";return{documentID:null,entity:null,error:{code:e,message:t instanceof Error?t.message:String(t),requiredScopes:t.requiredScopes}}}}})}}}export{n as DBCredentialProvider};
@@ -0,0 +1,45 @@
1
+ import { EventEmitter } from '@enkaku/event';
2
+ import type { ConnectorSyncEventPayload, SyncBoundary } from '@kubun/connector';
3
+ import type { Logger } from '@kubun/logger';
4
+ import type { ConnectorRegistry } from './registry.js';
5
+ type SyncEvents = {
6
+ sync: ConnectorSyncEventPayload;
7
+ };
8
+ export type ConnectorActivatedEvent = {
9
+ connectorName: string;
10
+ graphID: string;
11
+ };
12
+ export type ConnectorDeactivatedEvent = {
13
+ connectorName: string;
14
+ };
15
+ export type ConnectorManagerEvents = {
16
+ 'connector:activated': ConnectorActivatedEvent;
17
+ 'connector:deactivated': ConnectorDeactivatedEvent;
18
+ };
19
+ export type ConnectorManagerParams = {
20
+ registry: ConnectorRegistry;
21
+ logger: Logger;
22
+ defaults?: {
23
+ boundary?: SyncBoundary;
24
+ pollingInterval?: string;
25
+ };
26
+ };
27
+ export declare class ConnectorManager {
28
+ #private;
29
+ constructor(params: ConnectorManagerParams);
30
+ get defaults(): {
31
+ boundary: SyncBoundary;
32
+ pollingInterval: string;
33
+ };
34
+ activateForGraph(graphID: string, connectorNames: Array<string>): void;
35
+ deactivateForGraph(graphID: string, connectorNames: Array<string>): void;
36
+ isActive(connectorName: string): boolean;
37
+ getActiveGraphs(connectorName: string): Array<string>;
38
+ listActive(): Array<string>;
39
+ onConnectorActivated(callback: (event: ConnectorActivatedEvent) => void): () => void;
40
+ onConnectorDeactivated(callback: (event: ConnectorDeactivatedEvent) => void): () => void;
41
+ get syncEvents(): EventEmitter<SyncEvents>;
42
+ emitSyncEvent(event: ConnectorSyncEventPayload): Promise<void>;
43
+ onSyncEvent(callback: (event: ConnectorSyncEventPayload) => void): () => void;
44
+ }
45
+ export {};
package/lib/manager.js ADDED
@@ -0,0 +1 @@
1
+ import{EventEmitter as t}from"@enkaku/event";export class ConnectorManager{#t;#e;#n=new Map;#r=new t;#s=new t;#i;constructor(t){this.#t=t.registry,this.#e=t.logger,this.#i={boundary:t.defaults?.boundary??{maxAge:"P90D"},pollingInterval:t.defaults?.pollingInterval??"PT5M"}}get defaults(){return this.#i}activateForGraph(t,e){for(let n of e){if(!this.#t.has(n))throw Error(`Connector "${n}" is not registered`);let e=this.#n.get(n),r=null==e||0===e.size;null==e&&(e=new Set,this.#n.set(n,e)),e.add(t),r&&(this.#e.info("Connector {name} activated by graph {graphID}",{name:n,graphID:t}),this.#r.emit("connector:activated",{connectorName:n,graphID:t}))}}deactivateForGraph(t,e){for(let n of e){let e=this.#n.get(n);null!=e&&(e.delete(t),0===e.size&&(this.#n.delete(n),this.#e.info("Connector {name} deactivated",{name:n}),this.#r.emit("connector:deactivated",{connectorName:n})))}}isActive(t){let e=this.#n.get(t);return null!=e&&e.size>0}getActiveGraphs(t){let e=this.#n.get(t);return e?Array.from(e):[]}listActive(){return Array.from(this.#n.keys()).filter(t=>this.isActive(t))}onConnectorActivated(t){return this.#r.on("connector:activated",t)}onConnectorDeactivated(t){return this.#r.on("connector:deactivated",t)}get syncEvents(){return this.#s}async emitSyncEvent(t){await this.#s.emit("sync",t)}onSyncEvent(t){return this.#s.on("sync",t)}}
@@ -0,0 +1,3 @@
1
+ import type { PluginMigrationContext } from '@kubun/engine';
2
+ import type { Migration } from 'kysely';
3
+ export declare function createConnectorMigrations(ctx: PluginMigrationContext): Record<string, Migration>;
@@ -0,0 +1 @@
1
+ export function createConnectorMigrations(t){let e=t.types;return{"001_create_connector_tables":{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()}}}}
package/lib/oauth.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ import type { Runtime } from '@enkaku/runtime';
2
+ import type { CredentialProvider, OAuthProviderDefinition } from '@kubun/connector';
3
+ import type { ConnectorRegistry } from './registry.js';
4
+ import type { CompleteConnectorAuthInput, CompleteConnectorAuthOutput, StartConnectorAuthInput, StartConnectorAuthOutput } from './schema.js';
5
+ export type OAuthServiceParams = {
6
+ runtime: Runtime;
7
+ providers: Array<OAuthProviderDefinition>;
8
+ registry: ConnectorRegistry;
9
+ credentialProvider: CredentialProvider;
10
+ };
11
+ export declare class OAuthService {
12
+ #private;
13
+ constructor(params: OAuthServiceParams);
14
+ startAuth(args: StartConnectorAuthInput): Promise<StartConnectorAuthOutput>;
15
+ completeAuth(args: CompleteConnectorAuthInput, viewerDID: string): Promise<CompleteConnectorAuthOutput>;
16
+ }
package/lib/oauth.js ADDED
@@ -0,0 +1 @@
1
+ export class OAuthService{#e;#r;#t;#i;constructor(e){this.#e=e.credentialProvider,this.#r=e.runtime,this.#t=e.providers,this.#i=e.registry}async startAuth(e){let r=this.#t.find(r=>r.name===e.provider);if(null==r)throw Error(`OAuth provider "${e.provider}" not found`);if(null==r.clientID)throw Error(`OAuth provider "${e.provider}" is missing clientID`);let t=!0===e.requestWriteAccess,i=new Set(r.baseScopes??[]);for(let r of e.connectors??this.#i.list()){let o=this.#i.get(r);if(null!=o&&o.auth.provider===e.provider){let e=o.auth;if(t&&null!=e.writeScopes)for(let r of e.writeScopes)i.add(r);else for(let r of e.scopes)i.add(r)}}let o=JSON.stringify({nonce:this.#r.getRandomID(),provider:e.provider}),s=new URL(r.authorizationEndpoint);return s.searchParams.set("client_id",r.clientID),s.searchParams.set("redirect_uri",e.redirectURL),s.searchParams.set("response_type","code"),s.searchParams.set("scope",Array.from(i).join(" ")),s.searchParams.set("state",o),s.searchParams.set("access_type","offline"),s.searchParams.set("prompt","consent"),{url:s.toString(),state:o}}async completeAuth(e,r){let t=this.#t.find(r=>r.name===e.provider);if(null==t)throw Error(`OAuth provider "${e.provider}" not found`);if(null==t.clientID||null==t.clientSecret)throw Error(`OAuth provider "${e.provider}" is missing clientID or clientSecret`);let i=await this.#r.fetch(t.tokenEndpoint,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({grant_type:"authorization_code",code:e.code,redirect_uri:e.redirectURL,client_id:t.clientID,client_secret:t.clientSecret})});if(!i.ok)throw Error(`Token exchange failed: ${i.statusText}`);let o=await i.json(),s=o.scope?.split(" ")??[];return await this.#e.set(e.provider,r,{accessToken:o.access_token,refreshToken:o.refresh_token,expiresAt:null!=o.expires_in?new Date(Date.now()+1e3*o.expires_in):void 0,scopes:s}),{providerName:e.provider,scopes:s}}}
@@ -0,0 +1,10 @@
1
+ import type { ConnectorDefinition } from '@kubun/connector';
2
+ export declare class ConnectorRegistry {
3
+ #private;
4
+ register(connector: ConnectorDefinition): void;
5
+ unregister(name: string): void;
6
+ get(name: string): ConnectorDefinition | undefined;
7
+ has(name: string): boolean;
8
+ list(): Array<string>;
9
+ getAll(): Array<ConnectorDefinition>;
10
+ }
@@ -0,0 +1 @@
1
+ export class ConnectorRegistry{#e=new Map;register(e){if(this.#e.has(e.name))throw Error(`Connector "${e.name}" is already registered`);this.#e.set(e.name,e)}unregister(e){this.#e.delete(e)}get(e){return this.#e.get(e)}has(e){return this.#e.has(e)}list(){return Array.from(this.#e.keys())}getAll(){return Array.from(this.#e.values())}}
@@ -0,0 +1,111 @@
1
+ import type { ConnectorSyncEventPayload } from '@kubun/connector';
2
+ import type { SchemaExtension } from '@kubun/engine';
3
+ import type { ConnectorRegistry } from './registry.js';
4
+ /**
5
+ * Combined connector state returned by the `connector` and `connectors` queries.
6
+ * Mirrors `ConnectorSyncStateResult` from `@kubun/graphql/src/context.ts`.
7
+ */
8
+ export type ConnectorState = {
9
+ name: string;
10
+ status: 'IDLE' | 'SYNCING' | 'ERROR';
11
+ authenticated: boolean;
12
+ hasWriteAccess: boolean;
13
+ authExpiresAt: string | null;
14
+ lastSyncedAt: string | null;
15
+ entityCount: number | null;
16
+ error: string | null;
17
+ };
18
+ /**
19
+ * Input/output types for connector auth mutations.
20
+ */
21
+ export type StartConnectorAuthInput = {
22
+ provider: string;
23
+ redirectURL: string;
24
+ connectors?: Array<string> | null;
25
+ requestWriteAccess?: boolean | null;
26
+ };
27
+ export type StartConnectorAuthOutput = {
28
+ url: string;
29
+ state: string;
30
+ };
31
+ export type CompleteConnectorAuthInput = {
32
+ provider: string;
33
+ code: string;
34
+ redirectURL: string;
35
+ state?: string | null;
36
+ };
37
+ export type CompleteConnectorAuthOutput = {
38
+ providerName: string;
39
+ scopes: Array<string>;
40
+ };
41
+ export type SyncTriggerInput = {
42
+ connector: string;
43
+ full?: boolean | null;
44
+ };
45
+ export type SyncTriggerOutput = {
46
+ status: 'STARTED' | 'ALREADY_SYNCING' | 'ERROR';
47
+ connectorName: string;
48
+ };
49
+ /**
50
+ * Input for executing a connector action (create/update).
51
+ */
52
+ export type ConnectorActionInput = {
53
+ connector: string;
54
+ action: 'create' | 'update';
55
+ model: string;
56
+ input: Record<string, unknown>;
57
+ sourceID?: string;
58
+ };
59
+ /**
60
+ * Structured error returned when a connector action fails.
61
+ */
62
+ export type ConnectorActionError = {
63
+ code: string;
64
+ message: string;
65
+ requiredScopes?: Array<string>;
66
+ };
67
+ /**
68
+ * Result of executing a connector action.
69
+ */
70
+ export type ConnectorActionResult = {
71
+ documentID: string | null;
72
+ entity: Record<string, unknown> | null;
73
+ error: ConnectorActionError | null;
74
+ };
75
+ /**
76
+ * Connector sync event payload for subscriptions (GraphQL result type).
77
+ */
78
+ export type ConnectorSyncEventResult = {
79
+ type: 'STARTED' | 'PROGRESS' | 'COMPLETED' | 'ERROR';
80
+ connectorName: string;
81
+ entitiesProcessed: number | null;
82
+ entitiesFailed: number | null;
83
+ totalProcessed: number | null;
84
+ totalFailed: number | null;
85
+ duration: number | null;
86
+ error: string | null;
87
+ };
88
+ /**
89
+ * Transform a raw `ConnectorSyncEventPayload` into the GraphQL result shape.
90
+ */
91
+ export declare function toConnectorSyncEventResult(event: ConnectorSyncEventPayload): ConnectorSyncEventResult;
92
+ /**
93
+ * Per-request context methods for the Connector query and mutation fields.
94
+ * Provided by the context factory when registry + credentials are configured.
95
+ */
96
+ export type ConnectorQueryContext = {
97
+ getConnectorState: (name: string) => Promise<ConnectorState>;
98
+ getConnectorStates: () => Promise<Array<ConnectorState>>;
99
+ startConnectorAuth: (args: StartConnectorAuthInput) => Promise<StartConnectorAuthOutput>;
100
+ completeConnectorAuth: (args: CompleteConnectorAuthInput) => Promise<CompleteConnectorAuthOutput>;
101
+ triggerConnectorSync: (args: SyncTriggerInput) => Promise<SyncTriggerOutput>;
102
+ subscribeToConnectorSyncEvents: (connector?: string | null) => AsyncIterable<ConnectorSyncEventResult>;
103
+ executeConnectorAction?: (args: ConnectorActionInput) => Promise<ConnectorActionResult>;
104
+ };
105
+ export type ConnectorExtensionConfig = {
106
+ connectors?: Array<string>;
107
+ };
108
+ export declare function createConnectorSchemaExtension(params: {
109
+ registry?: ConnectorRegistry;
110
+ config: ConnectorExtensionConfig;
111
+ }): SchemaExtension;
package/lib/schema.js ADDED
@@ -0,0 +1,96 @@
1
+ import{PluginNodeID as t}from"@kubun/id";export function toConnectorSyncEventResult(t){return{type:"started"===t.type?"STARTED":"progress"===t.type?"PROGRESS":"completed"===t.type?"COMPLETED":"ERROR",connectorName:t.connectorName,entitiesProcessed:t.entitiesProcessed??null,entitiesFailed:t.entitiesFailed??null,totalProcessed:t.totalProcessed??null,totalFailed:t.totalFailed??null,duration:t.duration??null,error:t.error??null}}function e(t){let e=[];for(let n of Object.values(t.clusters))for(let t of Object.values(n.record)){let o=n.models[t];o?.name==null||e.includes(o.name)||e.push(o.name)}return e}export function createConnectorSchemaExtension(n){let o=n.registry,r=[];r.push(`
2
+ enum ConnectorSyncStatus {
3
+ IDLE
4
+ SYNCING
5
+ ERROR
6
+ }
7
+
8
+ enum SyncTriggerStatus {
9
+ STARTED
10
+ ALREADY_SYNCING
11
+ ERROR
12
+ }
13
+
14
+ enum ConnectorSyncEventType {
15
+ STARTED
16
+ PROGRESS
17
+ COMPLETED
18
+ ERROR
19
+ }
20
+
21
+ type Connector implements Node {
22
+ id: ID!
23
+ name: String!
24
+ status: ConnectorSyncStatus!
25
+ authenticated: Boolean!
26
+ hasWriteAccess: Boolean!
27
+ authExpiresAt: String
28
+ lastSyncedAt: String
29
+ entityCount: Int
30
+ error: String
31
+ }
32
+
33
+ type StartConnectorAuthResult {
34
+ url: String!
35
+ state: String!
36
+ }
37
+
38
+ type CompleteConnectorAuthResult {
39
+ providerName: String!
40
+ scopes: [String!]!
41
+ }
42
+
43
+ type SyncTriggerResult {
44
+ status: SyncTriggerStatus!
45
+ connectorName: String!
46
+ }
47
+
48
+ type ConnectorSyncEvent {
49
+ type: ConnectorSyncEventType!
50
+ connectorName: String!
51
+ entitiesProcessed: Int
52
+ entitiesFailed: Int
53
+ totalProcessed: Int
54
+ totalFailed: Int
55
+ duration: Float
56
+ error: String
57
+ }
58
+
59
+ type ConnectorActionError {
60
+ code: String!
61
+ message: String!
62
+ requiredScopes: [String!]
63
+ }
64
+
65
+ extend type Query {
66
+ connector(name: String!): Connector!
67
+ connectors: [Connector!]!
68
+ }
69
+
70
+ extend type Mutation {
71
+ startConnectorAuth(provider: String!, redirectURL: String!, connectors: [String!], requestWriteAccess: Boolean): StartConnectorAuthResult!
72
+ completeConnectorAuth(provider: String!, code: String!, redirectURL: String!, state: String): CompleteConnectorAuthResult!
73
+ syncConnector(connector: String!, full: Boolean): SyncTriggerResult!
74
+ }
75
+
76
+ extend type Subscription {
77
+ connectorSyncEvents(connector: String): ConnectorSyncEvent!
78
+ }
79
+ `);let c=[];if(null!=o){let t=new Set;for(let n of o.getAll()){if(null==n.actions||0===n.actions.length)continue;let o=n.actions.some(t=>"create"===t.name),i=n.actions.some(t=>"update"===t.name);if(o||i)for(let s of e(n).filter(t=>"ExternalEntity"!==t))t.has(s)||(t.add(s),r.push(`
80
+ type Connector${s}Payload {
81
+ documentID: String
82
+ entity: JSON
83
+ error: ConnectorActionError
84
+ }
85
+ `)),o&&(r.push(`
86
+ input ConnectorCreate${s}Input {
87
+ input: JSON!
88
+ }
89
+ `),c.push(` connectorCreate${s}(input: ConnectorCreate${s}Input!): Connector${s}Payload!`)),i&&(r.push(`
90
+ input ConnectorUpdate${s}Input {
91
+ sourceID: String!
92
+ input: JSON!
93
+ }
94
+ `),c.push(` connectorUpdate${s}(input: ConnectorUpdate${s}Input!): Connector${s}Payload!`))}}c.length>0&&r.push(`extend type Mutation {
95
+ ${c.join("\n")}
96
+ }`);let i=r.join("\n"),s={startConnectorAuth:(t,e,n)=>n.startConnectorAuth(e),completeConnectorAuth:(t,e,n)=>n.completeConnectorAuth(e),syncConnector:(t,e,n)=>n.triggerConnectorSync(e)};if(null!=o)for(let t of o.getAll()){if(null==t.actions||0===t.actions.length)continue;let n=t.actions.some(t=>"create"===t.name),o=t.actions.some(t=>"update"===t.name);if(n||o)for(let r of e(t).filter(t=>"ExternalEntity"!==t)){if(n){let e=t.name;s[`connectorCreate${r}`]=async(t,n,o)=>{if(null==o.executeConnectorAction)throw Error("executeConnectorAction is not available in this context");let c=n.input;return await o.executeConnectorAction({connector:e,action:"create",model:r,input:c.input})}}if(o){let e=t.name;s[`connectorUpdate${r}`]=async(t,n,o)=>{if(null==o.executeConnectorAction)throw Error("executeConnectorAction is not available in this context");let c=n.input;return await o.executeConnectorAction({connector:e,action:"update",model:r,input:c.input,sourceID:c.sourceID})}}}}return{sdl:i,resolvers:{queryFields:{connector:(t,e,n)=>n.getConnectorState(e.name),connectors:(t,e,n)=>n.getConnectorStates()},mutationFields:s,subscriptionFields:{connectorSyncEvents:{resolve:t=>t,subscribe:(t,e,n)=>n.subscribeToConnectorSyncEvents(e.connector)}},typeFields:{Connector:{id:e=>t.create("connector","Connector",e.name).toString()}},nodeResolvers:{Connector:{resolve:(t,e)=>e.getConnectorState(t)}}}}}
@@ -0,0 +1,22 @@
1
+ import type { DataProvider, SyncBoundary, SyncEvent, SyncStateStore } from '@kubun/connector';
2
+ import type { KubunDB } from '@kubun/db';
3
+ import type { ClustersRecord } from '@kubun/protocol';
4
+ export type SyncEngineParams = {
5
+ db: KubunDB;
6
+ stateStore: SyncStateStore;
7
+ connectorName: string;
8
+ clusters: ClustersRecord;
9
+ };
10
+ export type RunSyncParams = {
11
+ provider: DataProvider;
12
+ ownerDID: string;
13
+ boundary: SyncBoundary;
14
+ full?: boolean;
15
+ signal?: AbortSignal;
16
+ };
17
+ export declare class SyncEngine {
18
+ #private;
19
+ constructor(params: SyncEngineParams);
20
+ on(event: 'sync', callback: (event: SyncEvent) => void): () => void;
21
+ run(params: RunSyncParams): Promise<void>;
22
+ }
@@ -0,0 +1 @@
1
+ import{EventEmitter as t}from"@enkaku/event";import{EntityProcessor as e}from"./processor.js";export class SyncEngine{#t;#e;#n;#s;#o;#c=new Map;#r=new t;constructor(t){for(let e of(this.#t=t.db,this.#e=t.stateStore,this.#n=t.connectorName,this.#s=t.clusters,this.#o=new Map,Object.values(t.clusters)))for(let[t,n]of Object.entries(e.record))this.#o.set(t,e.models[n])}on(t,e){return this.#r.on(t,e)}#i(t,n){let s=this.#c.get(t);if(null!=s)return s;let o=this.#o.get(t);if(null==o)throw Error(`Unknown model ID "${t}" in connector "${this.#n}"`);let c=new e({db:this.#t,modelID:t,ownerDID:n,connectorName:this.#n,edgeFields:o.fieldsMeta,clusters:this.#s});return this.#c.set(t,c),c}async run(t){let{provider:e,ownerDID:n,boundary:s,full:o,signal:c}=t,r=Date.now(),i=await this.#e.get(this.#n,n),a=!o&&i?.checkpoint!=null,l=a?"incremental":"initial";await this.#e.set(this.#n,n,{checkpoint:i?.checkpoint??null,lastSyncedAt:i?.lastSyncedAt??new Date().toISOString(),entityCount:i?.entityCount??0,status:"syncing"});let h=0,d=0,y=0,m=i?.checkpoint??null;try{let t={boundary:s,checkpoint:a?i?.checkpoint:void 0,signal:c};for await(let s of a?e.fetchChanges({...t,lastSyncedAt:i?.lastSyncedAt?new Date(i.lastSyncedAt):void 0}):e.fetchAll(t)){y++;let t=this.#i(s.modelID,n),e=await t.processBatch(s);h+=e.created+e.updated+e.deleted,d+=e.failed,null!=s.checkpoint&&(m=s.checkpoint);let o={type:"sync:progress",connectorName:this.#n,phase:l,entitiesProcessed:h,entitiesFailed:d,currentBatch:y,checkpoint:m};await this.#r.emit("sync",o),await this.#e.set(this.#n,n,{checkpoint:m,lastSyncedAt:new Date().toISOString(),entityCount:(i?.entityCount??0)+h,status:"syncing"})}let o=Date.now()-r;await this.#e.set(this.#n,n,{checkpoint:m,lastSyncedAt:new Date().toISOString(),entityCount:(i?.entityCount??0)+h,status:"idle"});let u={type:"sync:complete",connectorName:this.#n,totalProcessed:h,totalFailed:d,duration:o};await this.#r.emit("sync",u)}catch(s){let t=s instanceof Error?s.message:String(s);await this.#e.set(this.#n,n,{checkpoint:m,lastSyncedAt:i?.lastSyncedAt??new Date().toISOString(),entityCount:i?.entityCount??0,status:"error",error:t});let e={type:"sync:error",connectorName:this.#n,error:t};await this.#r.emit("sync",e)}}}
@@ -0,0 +1,26 @@
1
+ import type { ConnectorSyncEventPayload, CredentialProvider, SyncBoundary, SyncStateStore } from '@kubun/connector';
2
+ import type { KubunDB } from '@kubun/db';
3
+ import type { ConnectorRegistry } from '../registry.js';
4
+ /**
5
+ * Minimal interface for the sync event emitter dependency.
6
+ * ConnectorManager (not yet ported) satisfies this — when it is ported,
7
+ * this type can be replaced with the concrete class.
8
+ */
9
+ export type SyncEventEmitter = {
10
+ emitSyncEvent(event: ConnectorSyncEventPayload): Promise<void>;
11
+ };
12
+ export type OrchestrateSyncParams = {
13
+ connectorName: string;
14
+ full: boolean;
15
+ registry: ConnectorRegistry;
16
+ syncEventEmitter: SyncEventEmitter;
17
+ stateStore: SyncStateStore;
18
+ credentialProvider: CredentialProvider;
19
+ ownerDID: string;
20
+ db: KubunDB;
21
+ boundary?: SyncBoundary;
22
+ };
23
+ export type OrchestrateSyncResult = {
24
+ status: 'started' | 'already_syncing' | 'error';
25
+ };
26
+ export declare function orchestrateSync(params: OrchestrateSyncParams): Promise<OrchestrateSyncResult>;
@@ -0,0 +1 @@
1
+ import{SyncEngine as e}from"./engine.js";export async function orchestrateSync(t){let{connectorName:r,full:n,registry:s,syncEventEmitter:a,stateStore:i,credentialProvider:o,ownerDID:c,db:l}=t,u=s.get(r);if(null==u)return{status:"error"};let y=await i.get(r,c);if(y?.status==="syncing")return{status:"already_syncing"};let d="device"!==u.auth.provider?u.auth.provider:null,p=d?await o.get(d,c):null;if(null!=d&&null==p||null==u.serverProvider)return{status:"error"};let v=t.boundary??{maxAge:"P90D"},g=u.serverProvider({credential:p??{accessToken:"",scopes:[]},boundary:v}),m=new e({db:l,stateStore:i,connectorName:r,clusters:u.clusters});return await a.emitSyncEvent({type:"started",connectorName:r}),m.on("sync",e=>{"sync:progress"===e.type?a.emitSyncEvent({type:"progress",connectorName:r,entitiesProcessed:e.entitiesProcessed,entitiesFailed:e.entitiesFailed}):"sync:complete"===e.type?a.emitSyncEvent({type:"completed",connectorName:r,totalProcessed:e.totalProcessed,totalFailed:e.totalFailed,duration:e.duration}):"sync:error"===e.type&&a.emitSyncEvent({type:"error",connectorName:r,error:e.error})}),m.run({provider:g,ownerDID:c,boundary:v,full:n}).catch(async e=>{await a.emitSyncEvent({type:"error",connectorName:r,error:e instanceof Error?e.message:String(e)})}),{status:"started"}}
@@ -0,0 +1,23 @@
1
+ import type { EntityBatch } from '@kubun/connector';
2
+ import type { KubunDB, WritableDB } from '@kubun/db';
3
+ import type { ClustersRecord, DocumentFieldsMeta } from '@kubun/protocol';
4
+ export type ProcessBatchResult = {
5
+ created: number;
6
+ updated: number;
7
+ deleted: number;
8
+ failed: number;
9
+ documentIDs: Array<string>;
10
+ };
11
+ export type EntityProcessorParams = {
12
+ db: KubunDB | WritableDB;
13
+ modelID: string;
14
+ ownerDID: string;
15
+ connectorName: string;
16
+ edgeFields?: DocumentFieldsMeta;
17
+ clusters?: ClustersRecord;
18
+ };
19
+ export declare class EntityProcessor {
20
+ #private;
21
+ constructor(params: EntityProcessorParams);
22
+ processBatch(batch: Pick<EntityBatch, 'entities' | 'deleted'>): Promise<ProcessBatchResult>;
23
+ }
@@ -0,0 +1 @@
1
+ import{DocumentID as e,DocumentModelID as t}from"@kubun/id";export class EntityProcessor{#e;#t;#r;#o;#c;#n;constructor(e){this.#e=e.db,this.#t=t.fromString(e.modelID),this.#r=e.ownerDID,this.#o=e.connectorName,this.#c=e.edgeFields??{},this.#n=e.clusters}#s(t,r){let o=new TextEncoder().encode(`${t}:${r}`);return e.create(this.#t,this.#r,o)}#i(e){let r,o=this.#n;if(null==o)return;for(let t of Object.values(o))if(null!=t.record[e])return e;try{r=t.fromString(e)}catch{return}if(!r.isLocal)return;let c=r.index;for(let e of Object.values(o))for(let[t,r]of Object.entries(e.record))if(r===c)return t}#l(r){if(null==this.#n||0===Object.keys(this.#c).length)return r;let o={...r};for(let[c,n]of Object.entries(this.#c)){if("document"!==n.type||null==n.model)continue;let s=r[c];if(null==s||"object"!=typeof s)continue;let i=s.sourceService,l=s.sourceID;if("string"!=typeof i||"string"!=typeof l)continue;let u=this.#i(n.model);if(null==u)continue;let d=new TextEncoder().encode(`${i}:${l}`),D=e.create(t.fromString(u),this.#r,d);o[c]=D.toString()}return o}async processBatch(e){let t=0,r=0,o=0,c=0,n=[];for(let o of e.entities)try{let e=this.#s(o.sourceService,o.sourceID),c=await this.#e.getDocument(e),s={...o,lastSyncedAt:new Date().toISOString()},i=this.#l(s);if(null==c){let r=new TextEncoder().encode(`${o.sourceService}:${o.sourceID}`);await this.#e.createDocument({id:e,owner:this.#r,unique:r,data:i}),t++}else await this.#e.saveDocument({id:e,existing:c,data:i}),r++;n.push(e.toString())}catch{c++}if(e.deleted)for(let t of e.deleted)try{let e=this.#s(t.sourceService,t.sourceID),r=await this.#e.getDocument(e);null!=r&&(await this.#e.saveDocument({id:e,existing:r,data:{sourceService:t.sourceService,sourceID:t.sourceID,lastSyncedAt:new Date().toISOString(),_deleted:!0}}),o++)}catch{c++}return{created:t,updated:r,deleted:o,failed:c,documentIDs:n}}}
@@ -0,0 +1,17 @@
1
+ import type { SyncState, SyncStateEntry, SyncStateStore } from '@kubun/connector';
2
+ import type { ConnectorAPI } from '../api.js';
3
+ /**
4
+ * DB-backed SyncStateStore that wraps ConnectorAPI methods.
5
+ * Maps between the SyncState type (with unknown checkpoint) and the
6
+ * SyncStateData type (with JSON-serialized string checkpoint).
7
+ */
8
+ export declare class DBSyncStateStore implements SyncStateStore {
9
+ #private;
10
+ constructor(params: {
11
+ api: ConnectorAPI;
12
+ });
13
+ get(connectorName: string, ownerDID: string): Promise<SyncState | null>;
14
+ set(connectorName: string, ownerDID: string, state: SyncState): Promise<void>;
15
+ delete(connectorName: string, ownerDID: string): Promise<void>;
16
+ listForConnector(connectorName: string): Promise<Array<SyncStateEntry>>;
17
+ }
@@ -0,0 +1 @@
1
+ export class DBSyncStateStore{#t;constructor(t){this.#t=t.api}async get(t,n){let e=await this.#t.getSyncState(t,n);return null==e?null:{checkpoint:null!=e.checkpoint?"string"==typeof e.checkpoint?JSON.parse(e.checkpoint):e.checkpoint:null,lastSyncedAt:e.lastSyncedAt,entityCount:e.entityCount,status:e.status,error:e.error??void 0}}async set(t,n,e){await this.#t.setSyncState(t,n,{checkpoint:null!=e.checkpoint?JSON.stringify(e.checkpoint):null,lastSyncedAt:e.lastSyncedAt,entityCount:e.entityCount,status:e.status,error:e.error??null})}async delete(t,n){await this.#t.deleteSyncState(t,n)}async listForConnector(t){return(await this.#t.listSyncStates(t)).map(t=>({connectorName:t.connectorName,ownerDID:t.ownerDID,checkpoint:null!=t.checkpoint?"string"==typeof t.checkpoint?JSON.parse(t.checkpoint):t.checkpoint:null,lastSyncedAt:t.lastSyncedAt,entityCount:t.entityCount,status:t.status,error:t.error??void 0}))}}
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@kubun/plugin-connector",
3
+ "version": "0.8.0",
4
+ "license": "see LICENSE.md",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "types": "lib/index.d.ts",
8
+ "exports": {
9
+ ".": "./lib/index.js"
10
+ },
11
+ "files": [
12
+ "lib/*",
13
+ "LICENSE.md"
14
+ ],
15
+ "sideEffects": false,
16
+ "dependencies": {
17
+ "@enkaku/async": "^0.14.0",
18
+ "@enkaku/event": "^0.14.1",
19
+ "@enkaku/generator": "^0.14.0",
20
+ "@enkaku/runtime": "^0.14.0",
21
+ "graphql": "^16.13.2",
22
+ "graphql-scalars": "^1.25.0",
23
+ "kysely": "^0.28.16",
24
+ "@kubun/connector": "^0.8.0",
25
+ "@kubun/db": "^0.8.0",
26
+ "@kubun/engine": "^0.8.0",
27
+ "@kubun/graphql": "^0.8.0",
28
+ "@kubun/id": "^0.8.0",
29
+ "@kubun/logger": "^0.8.0",
30
+ "@kubun/protocol": "^0.8.0"
31
+ },
32
+ "devDependencies": {
33
+ "@testcontainers/postgresql": "^11.14.0",
34
+ "get-port": "^7.2.0",
35
+ "@kubun/db-postgres": "^0.8.0",
36
+ "@kubun/test-utils": "^0.8.0"
37
+ },
38
+ "scripts": {
39
+ "build:clean": "del lib",
40
+ "build:js": "swc src -d ./lib --config-file ../../swc.json --strip-leading-paths",
41
+ "build:types": "tsc --emitDeclarationOnly --skipLibCheck",
42
+ "build": "pnpm run build:clean && pnpm run build:js && pnpm run build:types",
43
+ "test:types": "tsc --noEmit -p tsconfig.test.json",
44
+ "test:unit": "vitest run",
45
+ "test": "pnpm run test:types && pnpm run test:unit"
46
+ }
47
+ }