@powerhousedao/reactor-api 1.7.0 → 1.8.1

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/src/router.ts CHANGED
@@ -1,122 +1,170 @@
1
1
  import { ApolloServer } from "@apollo/server";
2
2
  import { expressMiddleware } from "@apollo/server/express4";
3
3
  import { ApolloServerPluginInlineTraceDisabled } from "@apollo/server/plugin/disabled";
4
+ import { PGlite } from "@electric-sql/pglite";
4
5
  import bodyParser from "body-parser";
5
6
  import cors from "cors";
6
7
  import { IDocumentDriveServer } from "document-drive";
8
+ import { drizzle as drizzlePg } from "drizzle-orm/node-postgres";
9
+ import { PgDatabase } from "drizzle-orm/pg-core";
10
+ import { drizzle as drizzlePglite } from "drizzle-orm/pglite";
7
11
  import express, { IRouter, Router } from "express";
12
+ import pg from "pg";
13
+ const { Pool } = pg;
8
14
  import {
9
15
  InternalListenerManager,
10
16
  InternalListenerModule,
11
17
  } from "./internal-listener-manager";
12
- import { SUBGRAPH_REGISTRY } from "./subgraphs";
13
- import { Context } from "./types";
14
- export let reactorRouter: IRouter = Router();
18
+ import { driveSubgraph, systemSubgraph } from "./subgraphs";
19
+ import { Context, Processor } from "./types";
20
+ import { createSchema } from "./utils/create-schema";
21
+ import { GraphQLSchema } from "graphql";
22
+
23
+ export class ReactorRouterManager {
24
+ private database: PgDatabase<any, any, any> | undefined;
25
+ private reactorRouter: IRouter = Router();
26
+ private contextFields: Record<string, any> = {};
27
+
28
+ // @todo: need to persist somewhere
29
+ private registry: Processor[] = [
30
+ {
31
+ name: "system",
32
+ resolvers: systemSubgraph.resolvers,
33
+ typeDefs: systemSubgraph.typeDefs,
34
+ },
35
+ {
36
+ name: "d/:drive",
37
+ resolvers: driveSubgraph.resolvers,
38
+ typeDefs: driveSubgraph.typeDefs,
39
+ },
40
+ ];
41
+ constructor(
42
+ private readonly path: string,
43
+ private readonly app: express.Express,
44
+ private readonly driveServer: IDocumentDriveServer,
45
+ private readonly client: PGlite | typeof Pool = new PGlite(),
46
+ private listenerManager: InternalListenerManager = new InternalListenerManager(
47
+ driveServer
48
+ )
49
+ ) {}
50
+
51
+ async init() {
52
+ if (this.client instanceof Pool) {
53
+ this.database = drizzlePg(this.client);
54
+ } else {
55
+ this.database = drizzlePglite(this.client as PGlite);
56
+ }
57
+ await this.listenerManager.init();
58
+ const models = this.driveServer.getDocumentModels();
59
+ const driveModel = models.find(
60
+ (it) => it.documentModel.name === "DocumentDrive"
61
+ );
62
+ if (!driveModel) {
63
+ throw new Error("DocumentDrive model required");
64
+ }
15
65
 
16
- const getLocalSubgraphConfig = (subgraphName: string) =>
17
- SUBGRAPH_REGISTRY.find((it) => it.name === subgraphName);
66
+ this.driveServer.on("documentModels", () => {
67
+ this.updateRouter().catch((error: unknown) => console.error(error));
68
+ });
18
69
 
19
- let listenerManager: InternalListenerManager | undefined;
70
+ this.app.use(this.path, (req, res, next) =>
71
+ this.reactorRouter(req, res, next)
72
+ );
20
73
 
21
- export const getListenerManager = async (driveServer: IDocumentDriveServer) => {
22
- if (!listenerManager) {
23
- listenerManager = new InternalListenerManager(driveServer);
24
- await listenerManager.init();
74
+ await this.updateRouter();
25
75
  }
26
- return listenerManager;
27
- };
28
-
29
- export const updateRouter = async (driveServer: IDocumentDriveServer) => {
30
- const newRouter = Router();
31
- newRouter.use(cors());
32
- newRouter.use(bodyParser.json());
33
- // Run each subgraph on the same http server, but at different paths
34
- for (const subgraph of SUBGRAPH_REGISTRY) {
35
- const subgraphConfig = getLocalSubgraphConfig(subgraph.name);
36
- if (!subgraphConfig) continue;
37
-
38
- // get schema
39
- const schema = subgraphConfig.getSchema(driveServer);
40
-
41
- // create apollo server
42
- const server = new ApolloServer({
43
- schema,
44
- introspection: true,
45
- plugins: [ApolloServerPluginInlineTraceDisabled()],
46
- });
47
76
 
48
- // start apollo server
49
- await server.start();
50
-
51
- // setup path
52
- const path = `/${subgraphConfig.name}`;
53
- newRouter.use(
54
- path,
55
- // @ts-ignore
56
- expressMiddleware(server, {
57
- context: ({ req }): Promise<Context> =>
58
- Promise.resolve({
77
+ async updateRouter() {
78
+ if (!this.database) {
79
+ await this.init();
80
+ }
81
+
82
+ const newRouter = Router();
83
+ newRouter.use(cors());
84
+ newRouter.use(bodyParser.json());
85
+ // Run each subgraph on the same http server, but at different paths
86
+ for (const subgraph of this.registry) {
87
+ if (subgraph.options && subgraph.transmit) {
88
+ await this.#registerInternalListener({
89
+ name: subgraph.name,
90
+ options: subgraph.options,
91
+ transmit: subgraph.transmit,
92
+ });
93
+ }
94
+
95
+ const subgraphConfig = this.#getLocalSubgraphConfig(subgraph.name);
96
+ if (!subgraphConfig) continue;
97
+ console.log(`Setting up subgraph ${subgraphConfig.name}`);
98
+ // get schema
99
+ const schema = createSchema(
100
+ this.driveServer,
101
+ subgraphConfig.resolvers,
102
+ subgraphConfig.typeDefs
103
+ );
104
+ // create apollo server
105
+ const server = new ApolloServer({
106
+ schema,
107
+ introspection: true,
108
+ plugins: [ApolloServerPluginInlineTraceDisabled()],
109
+ });
110
+
111
+ // start apollo server
112
+ await server.start();
113
+
114
+ // setup path
115
+ const path = `/${subgraphConfig.name}`;
116
+ newRouter.use(
117
+ path,
118
+ // @ts-ignore
119
+ expressMiddleware(server, {
120
+ context: ({ req }): Context => ({
59
121
  headers: req.headers,
60
122
  driveId: req.params.drive ?? undefined,
61
- driveServer,
62
- ...getAdditionalContextFields(),
123
+ driveServer: this.driveServer,
124
+ db: this.database!,
125
+ // analyticStore: undefined, // TODO: add analytic store
126
+ ...this.getAdditionalContextFields(),
63
127
  }),
64
- }),
65
- );
66
- console.log(`Setting up [${subgraphConfig.name}] subgraph at ${path}`);
67
- }
128
+ })
129
+ );
130
+ }
68
131
 
69
- listenerManager = await getListenerManager(driveServer);
70
- reactorRouter = newRouter;
71
- console.log("All subgraphs started.");
72
- };
73
-
74
- let docDriveServer: IDocumentDriveServer;
75
- export const initReactorRouter = async (
76
- path: string,
77
- app: express.Express,
78
- driveServer: IDocumentDriveServer,
79
- ) => {
80
- docDriveServer = driveServer;
81
- const models = driveServer.getDocumentModels();
82
- const driveModel = models.find(
83
- (it) => it.documentModel.name === "DocumentDrive",
84
- );
85
-
86
- if (!driveModel) {
87
- throw new Error("DocumentDrive model required");
132
+ this.reactorRouter = newRouter;
133
+ console.log("Router updated.");
88
134
  }
89
135
 
90
- await updateRouter(driveServer);
136
+ async registerProcessor(processor: Processor) {
137
+ const schema = createSchema(
138
+ this.driveServer,
139
+ processor.resolvers,
140
+ processor.typeDefs
141
+ );
91
142
 
92
- driveServer.on("documentModels", () => {
93
- updateRouter(driveServer).catch((error: unknown) => console.error(error));
94
- });
143
+ this.registry.unshift({
144
+ ...processor,
145
+ });
95
146
 
96
- app.use(path, (req, res, next) => reactorRouter(req, res, next));
97
- };
147
+ // update router
148
+ console.log(`Registering [${processor.name}] processor.`);
149
+ await this.updateRouter();
150
+ }
98
151
 
99
- export const addSubgraph = async (
100
- subgraph: (typeof SUBGRAPH_REGISTRY)[number],
101
- ) => {
102
- SUBGRAPH_REGISTRY.unshift(subgraph);
103
- await updateRouter(docDriveServer);
104
- };
152
+ #getLocalSubgraphConfig(subgraphName: string) {
153
+ return this.registry.find((it) => it.name === subgraphName);
154
+ }
105
155
 
106
- export const registerInternalListener = async (
107
- module: InternalListenerModule,
108
- ) => {
109
- if (!listenerManager) {
110
- throw new Error("Listener manager not initialized");
156
+ async #registerInternalListener(module: InternalListenerModule) {
157
+ if (!this.listenerManager) {
158
+ throw new Error("Listener manager not initialized");
159
+ }
160
+ await this.listenerManager.registerInternalListener(module);
111
161
  }
112
- await listenerManager.registerInternalListener(module);
113
- };
114
162
 
115
- let contextFields = {};
116
- export const getAdditionalContextFields = () => {
117
- return contextFields;
118
- };
163
+ getAdditionalContextFields = () => {
164
+ return this.contextFields;
165
+ };
119
166
 
120
- export const setAdditionalContextFields = (fields: Record<string, any>) => {
121
- contextFields = { ...contextFields, ...fields };
122
- };
167
+ setAdditionalContextFields(fields: Record<string, any>) {
168
+ this.contextFields = { ...this.contextFields, ...fields };
169
+ }
170
+ }
package/src/server.ts CHANGED
@@ -1,23 +1,34 @@
1
+ import { PGlite } from "@electric-sql/pglite";
1
2
  import { IDocumentDriveServer } from "document-drive";
2
3
  import express, { Express } from "express";
3
- import { initReactorRouter } from "./router";
4
+ import pg from "pg";
5
+ const { Pool } = pg;
6
+ import { ReactorRouterManager } from "./router";
7
+
4
8
  type Options = {
5
9
  express?: Express;
6
10
  port?: number;
11
+ client?: PGlite | typeof Pool | undefined;
7
12
  };
8
13
 
9
14
  const DEFAULT_PORT = 4000;
10
15
 
11
16
  export async function startAPI(
12
17
  reactor: IDocumentDriveServer,
13
- options: Options,
18
+ options: Options
14
19
  ) {
15
20
  const port = options.port ?? DEFAULT_PORT;
16
21
  const app = options.express ?? express();
17
22
 
18
- await initReactorRouter("/", app, reactor);
23
+ const reactorRouterManager = new ReactorRouterManager(
24
+ "/",
25
+ app,
26
+ reactor,
27
+ options.client
28
+ );
29
+ await reactorRouterManager.init();
19
30
 
20
31
  app.listen(port);
21
32
 
22
- return app;
33
+ return { app, reactorRouterManager };
23
34
  }
@@ -0,0 +1,4 @@
1
+ import { typeDefs } from "./type-defs";
2
+ import { resolvers } from "./resolvers";
3
+
4
+ export { typeDefs, resolvers };
@@ -17,8 +17,9 @@ import {
17
17
  DocumentModelState,
18
18
  } from "document-model/document-model";
19
19
  import { Context } from "../types";
20
+ import { GraphQLResolverMap } from "@apollo/subgraph/dist/schema-helper";
20
21
 
21
- export const resolvers = {
22
+ export const resolvers: GraphQLResolverMap<Context> = {
22
23
  Query: {
23
24
  drive: async (_: unknown, args: unknown, ctx: Context) => {
24
25
  if (!ctx.driveId) throw new Error("Drive ID is required");
@@ -37,7 +38,7 @@ export const resolvers = {
37
38
  const dms = ctx.driveServer.getDocumentModels();
38
39
  const dm = dms.find(
39
40
  ({ documentModel }: { documentModel: DocumentModelState }) =>
40
- documentModel.id === document.documentType,
41
+ documentModel.id === document.documentType
41
42
  );
42
43
  const globalState = document.state.global;
43
44
  if (!globalState) throw new Error("Document not found");
@@ -62,7 +63,7 @@ export const resolvers = {
62
63
  registerPullResponderListener: async (
63
64
  _: unknown,
64
65
  { filter }: { filter: ListenerFilter },
65
- ctx: Context,
66
+ ctx: Context
66
67
  ) => {
67
68
  if (!ctx.driveId) throw new Error("Drive ID is required");
68
69
  const uuid = generateUUID();
@@ -86,12 +87,12 @@ export const resolvers = {
86
87
 
87
88
  const result = await ctx.driveServer.queueDriveAction(
88
89
  ctx.driveId,
89
- actions.addListener({ listener }),
90
+ actions.addListener({ listener })
90
91
  );
91
92
 
92
93
  if (result.status !== "SUCCESS" && result.error) {
93
94
  throw new Error(
94
- `Listener couldn't be registered: ${result.error.message}`,
95
+ `Listener couldn't be registered: ${result.error.message}`
95
96
  );
96
97
  }
97
98
 
@@ -100,7 +101,7 @@ export const resolvers = {
100
101
  pushUpdates: async (
101
102
  _: unknown,
102
103
  { strands }: { strands: StrandUpdateGraphQL[] },
103
- ctx: Context,
104
+ ctx: Context
104
105
  ) => {
105
106
  if (!ctx.driveId) throw new Error("Drive ID is required");
106
107
  const listenerRevisions: ListenerRevision[] = await Promise.all(
@@ -118,11 +119,11 @@ export const resolvers = {
118
119
  ? ctx.driveServer.queueOperations(
119
120
  s.driveId,
120
121
  s.documentId,
121
- operations,
122
+ operations
122
123
  )
123
124
  : ctx.driveServer.queueDriveOperations(
124
125
  s.driveId,
125
- operations as Operation<DocumentDriveAction | BaseAction>[],
126
+ operations as Operation<DocumentDriveAction | BaseAction>[]
126
127
  ));
127
128
 
128
129
  const scopeOperations = result.document?.operations[s.scope] ?? [];
@@ -147,7 +148,7 @@ export const resolvers = {
147
148
  status: result.status,
148
149
  error: result.error?.message || undefined,
149
150
  };
150
- }),
151
+ })
151
152
  );
152
153
 
153
154
  return listenerRevisions;
@@ -158,7 +159,7 @@ export const resolvers = {
158
159
  listenerId,
159
160
  revisions,
160
161
  }: { listenerId: string; revisions: ListenerRevision[] },
161
- ctx: Context,
162
+ ctx: Context
162
163
  ) => {
163
164
  if (!listenerId || !revisions) return false;
164
165
  if (!ctx.driveId) throw new Error("Drive ID is required");
@@ -175,12 +176,12 @@ export const resolvers = {
175
176
 
176
177
  const transmitter = (await ctx.driveServer.getTransmitter(
177
178
  ctx.driveId,
178
- listenerId,
179
+ listenerId
179
180
  )) as PullResponderTransmitter;
180
181
  const result = await transmitter.processAcknowledge(
181
182
  ctx.driveId ?? "1",
182
183
  listenerId,
183
- validEntries,
184
+ validEntries
184
185
  );
185
186
 
186
187
  return result;
@@ -191,12 +192,12 @@ export const resolvers = {
191
192
  strands: async (
192
193
  _: unknown,
193
194
  { listenerId, since }: { listenerId: string; since: string | undefined },
194
- ctx: Context,
195
+ ctx: Context
195
196
  ) => {
196
197
  if (!ctx.driveId) throw new Error("Drive ID is required");
197
198
  const listener = (await ctx.driveServer.getTransmitter(
198
199
  ctx.driveId,
199
- listenerId,
200
+ listenerId
200
201
  )) as PullResponderTransmitter;
201
202
  const strands = await listener.getStrands({ since });
202
203
  return strands.map((e) => ({
@@ -1,4 +1,4 @@
1
- type Query {
1
+ export const typeDefs = `type Query {
2
2
  system: System
3
3
  drive: DocumentDriveState
4
4
  document(id: ID!): IDocument
@@ -133,3 +133,4 @@ type System {
133
133
  type Sync {
134
134
  strands(listenerId: ID!, since: String): [StrandUpdate!]!
135
135
  }
136
+ `;
@@ -1,15 +1,3 @@
1
- import { getSchema as getSystemSchema } from "./system/subgraph";
2
- import { getSchema as getDriveSchema } from "./drive/subgraph";
3
-
1
+ export * as systemSubgraph from "./system";
2
+ export * as driveSubgraph from "./drive";
4
3
  export * from "./types";
5
-
6
- export const SUBGRAPH_REGISTRY = [
7
- {
8
- name: "system",
9
- getSchema: getSystemSchema,
10
- },
11
- {
12
- name: "d/:drive",
13
- getSchema: getDriveSchema,
14
- },
15
- ];
@@ -0,0 +1,4 @@
1
+ import { typeDefs } from "./type-defs";
2
+ import { resolvers } from "./resolvers";
3
+
4
+ export { typeDefs, resolvers };
@@ -1,7 +1,8 @@
1
1
  import { DriveInput } from "document-drive";
2
2
  import { Context } from "../types";
3
+ import { GraphQLResolverMap } from "@apollo/subgraph/dist/schema-helper";
3
4
 
4
- export const resolvers = {
5
+ export const resolvers: GraphQLResolverMap<Context> = {
5
6
  Query: {
6
7
  drives: async (parent: unknown, args: unknown, ctx: Context) => {
7
8
  const drives = await ctx.driveServer.getDrives();
@@ -0,0 +1,18 @@
1
+ export const typeDefs = `type Query {
2
+ drives: [String!]!
3
+ driveIdBySlug(slug: String!): String
4
+ }
5
+
6
+ type Mutation {
7
+ addDrive(global: DocumentDriveStateInput!): DocumentDriveState
8
+ deleteDrive(id: ID!): Boolean
9
+ setDriveIcon(id: String!, icon: String!): Boolean
10
+ setDriveName(id: String!, name: String!): Boolean
11
+ }
12
+
13
+ input DocumentDriveStateInput {
14
+ name: String
15
+ id: String
16
+ slug: String
17
+ icon: String
18
+ }`;
package/src/types.ts CHANGED
@@ -1,8 +1,22 @@
1
- import { IDocumentDriveServer } from "document-drive";
1
+ import {
2
+ IDocumentDriveServer,
3
+ InternalTransmitterUpdate,
4
+ Listener,
5
+ } from "document-drive";
6
+ import { PgDatabase } from "drizzle-orm/pg-core";
2
7
  import { IncomingHttpHeaders } from "http";
3
8
 
4
9
  export interface Context {
5
10
  headers: IncomingHttpHeaders;
6
11
  driveId: string | undefined;
7
12
  driveServer: IDocumentDriveServer;
13
+ db: PgDatabase<any, any, any>;
8
14
  }
15
+
16
+ export type Processor = {
17
+ name: string;
18
+ resolvers: any;
19
+ typeDefs: string;
20
+ options?: Omit<Listener, "driveId">;
21
+ transmit?: (strands: InternalTransmitterUpdate[]) => Promise<void>;
22
+ };
@@ -3,11 +3,12 @@ import { IDocumentDriveServer } from "document-drive";
3
3
  import { GraphQLResolverMap } from "@apollo/subgraph/dist/schema-helper";
4
4
  import { typeDefs as scalarsTypeDefs } from "@powerhousedao/scalars";
5
5
  import { parse } from "graphql";
6
+ import { Context } from "src/types";
6
7
 
7
8
  export const createSchema = (
8
9
  documentDriveServer: IDocumentDriveServer,
9
- resolvers: GraphQLResolverMap,
10
- typeDefs: string,
10
+ resolvers: GraphQLResolverMap<Context>,
11
+ typeDefs: string
11
12
  ) =>
12
13
  buildSubgraphSchema([
13
14
  {
@@ -18,7 +19,7 @@ export const createSchema = (
18
19
 
19
20
  export const getDocumentModelTypeDefs = (
20
21
  documentDriveServer: IDocumentDriveServer,
21
- typeDefs: string,
22
+ typeDefs: string
22
23
  ) => {
23
24
  const documentModels = documentDriveServer.getDocumentModels();
24
25
  let dmSchema = "";
@@ -31,7 +32,7 @@ export const getDocumentModelTypeDefs = (
31
32
  .replaceAll(`: Account`, `: ${documentModel.name}Account`)
32
33
  .replaceAll(`[Account!]!`, `[${documentModel.name}Account!]!`)
33
34
  .replaceAll("scalar DateTime", "")
34
- .replaceAll(/input (.*?) {[\s\S]*?}/g, ""),
35
+ .replaceAll(/input (.*?) {[\s\S]*?}/g, "")
35
36
  )
36
37
  .join("\n")};
37
38
 
@@ -45,7 +46,7 @@ export const getDocumentModelTypeDefs = (
45
46
  .replaceAll(/input (.*?) {[\s\S]*?}/g, "")
46
47
  .replaceAll("type AccountSnapshotLocalState", "")
47
48
  .replaceAll("type BudgetStatementLocalState", "")
48
- .replaceAll("type ScopeFrameworkLocalState", ""),
49
+ .replaceAll("type ScopeFrameworkLocalState", "")
49
50
  )
50
51
  .join("\n")};
51
52
 
@@ -1,18 +0,0 @@
1
- type Query {
2
- drives: [String!]!
3
- driveIdBySlug(slug: String!): String
4
- }
5
-
6
- type Mutation {
7
- addDrive(global: DocumentDriveStateInput!): DocumentDriveState
8
- deleteDrive(id: ID!): Boolean
9
- setDriveIcon(id: String!, icon: String!): Boolean
10
- setDriveName(id: String!, name: String!): Boolean
11
- }
12
-
13
- input DocumentDriveStateInput {
14
- name: String
15
- id: String
16
- slug: String
17
- icon: String
18
- }