@powerhousedao/reactor-api 6.0.0-dev.4 → 6.0.0-dev.40

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.
Files changed (155) hide show
  1. package/dist/codegen.js +1 -1
  2. package/dist/codegen.js.map +1 -1
  3. package/dist/src/config.d.ts +1 -2
  4. package/dist/src/config.d.ts.map +1 -1
  5. package/dist/src/config.js +1 -5
  6. package/dist/src/config.js.map +1 -1
  7. package/dist/src/graphql/auth/subgraph.js +1 -1
  8. package/dist/src/graphql/auth/subgraph.js.map +1 -1
  9. package/dist/src/graphql/base-subgraph.d.ts +2 -2
  10. package/dist/src/graphql/base-subgraph.d.ts.map +1 -1
  11. package/dist/src/graphql/base-subgraph.js.map +1 -1
  12. package/dist/src/graphql/document-model-subgraph.d.ts +95 -43
  13. package/dist/src/graphql/document-model-subgraph.d.ts.map +1 -1
  14. package/dist/src/graphql/document-model-subgraph.js +570 -79
  15. package/dist/src/graphql/document-model-subgraph.js.map +1 -1
  16. package/dist/src/graphql/drive-subgraph.d.ts.map +1 -1
  17. package/dist/src/graphql/drive-subgraph.js +1 -0
  18. package/dist/src/graphql/drive-subgraph.js.map +1 -1
  19. package/dist/src/graphql/graphql-manager.d.ts +7 -2
  20. package/dist/src/graphql/graphql-manager.d.ts.map +1 -1
  21. package/dist/src/graphql/graphql-manager.js +128 -42
  22. package/dist/src/graphql/graphql-manager.js.map +1 -1
  23. package/dist/src/graphql/index.d.ts +1 -0
  24. package/dist/src/graphql/index.d.ts.map +1 -1
  25. package/dist/src/graphql/index.js +1 -0
  26. package/dist/src/graphql/index.js.map +1 -1
  27. package/dist/src/graphql/reactor/adapters.d.ts +10 -2
  28. package/dist/src/graphql/reactor/adapters.d.ts.map +1 -1
  29. package/dist/src/graphql/reactor/adapters.js +35 -1
  30. package/dist/src/graphql/reactor/adapters.js.map +1 -1
  31. package/dist/src/graphql/reactor/factory.d.ts +1 -1
  32. package/dist/src/graphql/reactor/factory.d.ts.map +1 -1
  33. package/dist/src/graphql/reactor/factory.js +1 -1
  34. package/dist/src/graphql/reactor/factory.js.map +1 -1
  35. package/dist/src/graphql/reactor/gen/graphql.d.ts +84 -24
  36. package/dist/src/graphql/reactor/gen/graphql.d.ts.map +1 -1
  37. package/dist/src/graphql/reactor/gen/graphql.js +33 -9
  38. package/dist/src/graphql/reactor/gen/graphql.js.map +1 -1
  39. package/dist/src/graphql/reactor/index.d.ts +1 -1
  40. package/dist/src/graphql/reactor/index.d.ts.map +1 -1
  41. package/dist/src/graphql/reactor/index.js +1 -1
  42. package/dist/src/graphql/reactor/index.js.map +1 -1
  43. package/dist/src/graphql/reactor/requester.with-zod.d.ts.map +1 -1
  44. package/dist/src/graphql/reactor/requester.with-zod.js +100 -38
  45. package/dist/src/graphql/reactor/requester.with-zod.js.map +1 -1
  46. package/dist/src/graphql/reactor/resolvers.d.ts +41 -21
  47. package/dist/src/graphql/reactor/resolvers.d.ts.map +1 -1
  48. package/dist/src/graphql/reactor/resolvers.js +118 -64
  49. package/dist/src/graphql/reactor/resolvers.js.map +1 -1
  50. package/dist/src/graphql/reactor/schema.graphql +51 -31
  51. package/dist/src/graphql/reactor/subgraph.d.ts.map +1 -1
  52. package/dist/src/graphql/reactor/subgraph.js +91 -108
  53. package/dist/src/graphql/reactor/subgraph.js.map +1 -1
  54. package/dist/src/graphql/reactor/validation.d.ts +24 -0
  55. package/dist/src/graphql/reactor/validation.d.ts.map +1 -1
  56. package/dist/src/graphql/reactor/validation.js +15 -0
  57. package/dist/src/graphql/reactor/validation.js.map +1 -1
  58. package/dist/src/graphql/system/index.d.ts +0 -1
  59. package/dist/src/graphql/system/index.d.ts.map +1 -1
  60. package/dist/src/graphql/system/index.js +0 -1
  61. package/dist/src/graphql/system/index.js.map +1 -1
  62. package/dist/src/graphql/types.d.ts +3 -3
  63. package/dist/src/graphql/types.d.ts.map +1 -1
  64. package/dist/src/graphql/utils.d.ts.map +1 -1
  65. package/dist/src/graphql/utils.js +7 -3
  66. package/dist/src/graphql/utils.js.map +1 -1
  67. package/dist/src/packages/import-loader.d.ts +5 -3
  68. package/dist/src/packages/import-loader.d.ts.map +1 -1
  69. package/dist/src/packages/import-loader.js +19 -10
  70. package/dist/src/packages/import-loader.js.map +1 -1
  71. package/dist/src/packages/package-manager.d.ts +2 -2
  72. package/dist/src/packages/package-manager.d.ts.map +1 -1
  73. package/dist/src/packages/package-manager.js.map +1 -1
  74. package/dist/src/packages/types.d.ts +8 -4
  75. package/dist/src/packages/types.d.ts.map +1 -1
  76. package/dist/src/packages/util.d.ts +3 -2
  77. package/dist/src/packages/util.d.ts.map +1 -1
  78. package/dist/src/packages/util.js +1 -1
  79. package/dist/src/packages/util.js.map +1 -1
  80. package/dist/src/packages/vite-loader.d.ts +7 -6
  81. package/dist/src/packages/vite-loader.d.ts.map +1 -1
  82. package/dist/src/packages/vite-loader.js +21 -8
  83. package/dist/src/packages/vite-loader.js.map +1 -1
  84. package/dist/src/server.d.ts +16 -7
  85. package/dist/src/server.d.ts.map +1 -1
  86. package/dist/src/server.js +117 -59
  87. package/dist/src/server.js.map +1 -1
  88. package/dist/src/services/auth.service.d.ts.map +1 -1
  89. package/dist/src/services/auth.service.js +11 -20
  90. package/dist/src/services/auth.service.js.map +1 -1
  91. package/dist/src/tracing.js +1 -1
  92. package/dist/src/tracing.js.map +1 -1
  93. package/dist/src/types.d.ts +5 -5
  94. package/dist/src/types.d.ts.map +1 -1
  95. package/dist/src/utils/create-schema.d.ts +21 -1
  96. package/dist/src/utils/create-schema.d.ts.map +1 -1
  97. package/dist/src/utils/create-schema.js +290 -16
  98. package/dist/src/utils/create-schema.js.map +1 -1
  99. package/dist/src/utils/drive-url.d.ts +2 -0
  100. package/dist/src/utils/drive-url.d.ts.map +1 -0
  101. package/dist/src/utils/drive-url.js +3 -0
  102. package/dist/src/utils/drive-url.js.map +1 -0
  103. package/dist/src/utils/index.d.ts +1 -0
  104. package/dist/src/utils/index.d.ts.map +1 -1
  105. package/dist/src/utils/index.js +1 -0
  106. package/dist/src/utils/index.js.map +1 -1
  107. package/dist/test/document-drive-subgraph.test.d.ts +2 -0
  108. package/dist/test/document-drive-subgraph.test.d.ts.map +1 -0
  109. package/dist/test/document-drive-subgraph.test.js +186 -0
  110. package/dist/test/document-drive-subgraph.test.js.map +1 -0
  111. package/dist/test/document-model-subgraph-legacy-permissions.test.d.ts +2 -0
  112. package/dist/test/document-model-subgraph-legacy-permissions.test.d.ts.map +1 -0
  113. package/dist/test/document-model-subgraph-legacy-permissions.test.js +518 -0
  114. package/dist/test/document-model-subgraph-legacy-permissions.test.js.map +1 -0
  115. package/dist/test/document-model-subgraph-permissions.test.d.ts +2 -0
  116. package/dist/test/document-model-subgraph-permissions.test.d.ts.map +1 -0
  117. package/dist/test/document-model-subgraph-permissions.test.js +635 -0
  118. package/dist/test/document-model-subgraph-permissions.test.js.map +1 -0
  119. package/dist/test/document-model-subgraph.test.d.ts +2 -0
  120. package/dist/test/document-model-subgraph.test.d.ts.map +1 -0
  121. package/dist/test/document-model-subgraph.test.js +441 -0
  122. package/dist/test/document-model-subgraph.test.js.map +1 -0
  123. package/dist/test/permissions-integration.test.js +4 -4
  124. package/dist/test/permissions-integration.test.js.map +1 -1
  125. package/dist/test/reactor-client.test.js +4 -4
  126. package/dist/test/reactor-client.test.js.map +1 -1
  127. package/dist/test/reactor-resolvers.test.js +4 -8
  128. package/dist/test/reactor-resolvers.test.js.map +1 -1
  129. package/dist/test/reactor-subgraph-permissions.test.js +4 -7
  130. package/dist/test/reactor-subgraph-permissions.test.js.map +1 -1
  131. package/dist/test/subscriptions.test.js +2 -0
  132. package/dist/test/subscriptions.test.js.map +1 -1
  133. package/dist/test/two-reactor-gql-catchup-duplicate.test.d.ts +2 -0
  134. package/dist/test/two-reactor-gql-catchup-duplicate.test.d.ts.map +1 -0
  135. package/dist/test/two-reactor-gql-catchup-duplicate.test.js +264 -0
  136. package/dist/test/two-reactor-gql-catchup-duplicate.test.js.map +1 -0
  137. package/dist/test/two-reactor-gql-sync.test.js +86 -99
  138. package/dist/test/two-reactor-gql-sync.test.js.map +1 -1
  139. package/dist/test/utils/gql-resolver-bridge.d.ts.map +1 -1
  140. package/dist/test/utils/gql-resolver-bridge.js +4 -4
  141. package/dist/test/utils/gql-resolver-bridge.js.map +1 -1
  142. package/dist/tsconfig.tsbuildinfo +1 -1
  143. package/package.json +21 -11
  144. package/dist/src/graphql/system/system-subgraph.d.ts +0 -49
  145. package/dist/src/graphql/system/system-subgraph.d.ts.map +0 -1
  146. package/dist/src/graphql/system/system-subgraph.js +0 -130
  147. package/dist/src/graphql/system/system-subgraph.js.map +0 -1
  148. package/dist/test/system.test.d.ts +0 -2
  149. package/dist/test/system.test.d.ts.map +0 -1
  150. package/dist/test/system.test.js +0 -211
  151. package/dist/test/system.test.js.map +0 -1
  152. package/dist/test/three-reactor-gql-sync.test.d.ts +0 -2
  153. package/dist/test/three-reactor-gql-sync.test.d.ts.map +0 -1
  154. package/dist/test/three-reactor-gql-sync.test.js +0 -368
  155. package/dist/test/three-reactor-gql-sync.test.js.map +0 -1
@@ -1,104 +1,595 @@
1
1
  import { camelCase, kebabCase } from "change-case";
2
2
  import { addFile } from "document-drive";
3
3
  import { setName } from "document-model";
4
- import { generateDocumentModelSchemaLegacy, getDocumentModelSchemaName, } from "../utils/create-schema.js";
4
+ import { GraphQLError, Kind, parse } from "graphql";
5
+ import { generateDocumentModelSchema, getDocumentModelSchemaName, } from "../utils/create-schema.js";
5
6
  import { BaseSubgraph } from "./base-subgraph.js";
7
+ import { toGqlPhDocument } from "./reactor/adapters.js";
8
+ import { createEmptyDocument as createEmptyDocumentResolver, createGetParentIdsFn, documentChildren as documentChildrenResolver, documentParents as documentParentsResolver, document as documentResolver, findDocuments as findDocumentsResolver, } from "./reactor/resolvers.js";
6
9
  import { buildGraphQlDocument } from "./utils.js";
7
- export function generateDocumentModelResolversLegacy(documentModel, reactor) {
8
- const documentType = documentModel.documentModel.global.id;
9
- const documentName = getDocumentModelSchemaName(documentModel.documentModel.global);
10
- const operations = documentModel.documentModel.global.specifications
11
- .at(-1)
12
- ?.modules.flatMap((module) => module.operations.filter((op) => op.name)) ?? [];
13
- return {
14
- Query: {
15
- [documentName]: () => {
16
- return {
17
- getDocument: async (args) => {
18
- const { docId, driveId } = args;
19
- if (!docId) {
20
- throw new Error("Document id is required");
10
+ /**
11
+ * New document model subgraph that uses reactorClient instead of legacy reactor.
12
+ * This class auto-generates GraphQL queries and mutations for a document model.
13
+ */
14
+ export class DocumentModelSubgraph extends BaseSubgraph {
15
+ documentModel;
16
+ constructor(documentModel, args) {
17
+ super(args);
18
+ this.documentModel = documentModel;
19
+ this.name = kebabCase(documentModel.documentModel.global.name);
20
+ this.typeDefs = generateDocumentModelSchema(this.documentModel.documentModel.global, { useNewApi: true });
21
+ this.resolvers = this.generateResolvers();
22
+ }
23
+ /**
24
+ * Check if user has global read access (admin, user, or guest)
25
+ */
26
+ hasGlobalReadAccess(ctx) {
27
+ const isGlobalAdmin = ctx.isAdmin?.(ctx.user?.address ?? "");
28
+ const isGlobalUser = ctx.isUser?.(ctx.user?.address ?? "");
29
+ const isGlobalGuest = ctx.isGuest?.(ctx.user?.address ?? "") ||
30
+ process.env.FREE_ENTRY === "true";
31
+ return !!(isGlobalAdmin || isGlobalUser || isGlobalGuest);
32
+ }
33
+ /**
34
+ * Check if user has global write access (admin or user, not guest)
35
+ */
36
+ hasGlobalWriteAccess(ctx) {
37
+ const isGlobalAdmin = ctx.isAdmin?.(ctx.user?.address ?? "");
38
+ const isGlobalUser = ctx.isUser?.(ctx.user?.address ?? "");
39
+ return !!(isGlobalAdmin || isGlobalUser);
40
+ }
41
+ /**
42
+ * Get the parent IDs function for hierarchical permission checks.
43
+ * Uses the shared createGetParentIdsFn from reactor/resolvers.
44
+ */
45
+ getParentIdsFn() {
46
+ return createGetParentIdsFn(this.reactorClient);
47
+ }
48
+ /**
49
+ * Check if user can read a document (with hierarchy)
50
+ */
51
+ async canReadDocument(documentId, ctx) {
52
+ if (this.hasGlobalReadAccess(ctx)) {
53
+ return true;
54
+ }
55
+ if (this.documentPermissionService) {
56
+ return this.documentPermissionService.canRead(documentId, ctx.user?.address, this.getParentIdsFn());
57
+ }
58
+ return false;
59
+ }
60
+ /**
61
+ * Check if user can write to a document (with hierarchy)
62
+ */
63
+ async canWriteDocument(documentId, ctx) {
64
+ if (this.hasGlobalWriteAccess(ctx)) {
65
+ return true;
66
+ }
67
+ if (this.documentPermissionService) {
68
+ return this.documentPermissionService.canWrite(documentId, ctx.user?.address, this.getParentIdsFn());
69
+ }
70
+ return false;
71
+ }
72
+ /**
73
+ * Throw an error if user cannot read the document
74
+ */
75
+ async assertCanRead(documentId, ctx) {
76
+ const canRead = await this.canReadDocument(documentId, ctx);
77
+ if (!canRead) {
78
+ throw new GraphQLError("Forbidden: insufficient permissions to read this document");
79
+ }
80
+ }
81
+ /**
82
+ * Throw an error if user cannot write to the document
83
+ */
84
+ async assertCanWrite(documentId, ctx) {
85
+ const canWrite = await this.canWriteDocument(documentId, ctx);
86
+ if (!canWrite) {
87
+ throw new GraphQLError("Forbidden: insufficient permissions to write to this document");
88
+ }
89
+ }
90
+ /**
91
+ * Check if user can execute a specific operation on a document.
92
+ * Throws an error if the operation is restricted and user lacks permission.
93
+ */
94
+ async assertCanExecuteOperation(documentId, operationType, ctx) {
95
+ if (!this.documentPermissionService) {
96
+ return;
97
+ }
98
+ if (ctx.isAdmin?.(ctx.user?.address ?? "")) {
99
+ return;
100
+ }
101
+ const isRestricted = await this.documentPermissionService.isOperationRestricted(documentId, operationType);
102
+ if (isRestricted) {
103
+ const canExecute = await this.documentPermissionService.canExecuteOperation(documentId, operationType, ctx.user?.address);
104
+ if (!canExecute) {
105
+ throw new GraphQLError(`Forbidden: insufficient permissions to execute operation "${operationType}" on this document`);
106
+ }
107
+ }
108
+ }
109
+ /**
110
+ * Generate __resolveType functions for union types found in the document model schema.
111
+ * Parses the state schema to find union definitions and their member types,
112
+ * then uses unique field presence to discriminate between member types at runtime.
113
+ */
114
+ generateUnionResolvers() {
115
+ const documentName = getDocumentModelSchemaName(this.documentModel.documentModel.global);
116
+ const specification = this.documentModel.documentModel.global.specifications.at(-1);
117
+ if (!specification)
118
+ return {};
119
+ const globalSchema = specification.state.global.schema ?? "";
120
+ const localSchema = specification.state.local.schema ?? "";
121
+ const fullSchema = `${globalSchema}\n${localSchema}`;
122
+ if (!fullSchema.trim())
123
+ return {};
124
+ let ast;
125
+ try {
126
+ ast = parse(fullSchema);
127
+ }
128
+ catch {
129
+ return {};
130
+ }
131
+ // Build map: object type name -> field names
132
+ const objectFieldsMap = new Map();
133
+ for (const def of ast.definitions) {
134
+ if (def.kind === Kind.OBJECT_TYPE_DEFINITION) {
135
+ objectFieldsMap.set(def.name.value, def.fields?.map((f) => f.name.value) ?? []);
136
+ }
137
+ }
138
+ const resolvers = {};
139
+ for (const def of ast.definitions) {
140
+ if (def.kind !== Kind.UNION_TYPE_DEFINITION)
141
+ continue;
142
+ const unionName = def.name.value;
143
+ const memberTypes = def.types?.map((t) => t.name.value) ?? [];
144
+ if (memberTypes.length === 0)
145
+ continue;
146
+ // Compute unique fields per member type
147
+ const uniqueFields = {};
148
+ for (const memberType of memberTypes) {
149
+ const ownFields = objectFieldsMap.get(memberType) ?? [];
150
+ const otherFields = new Set(memberTypes
151
+ .filter((t) => t !== memberType)
152
+ .flatMap((t) => objectFieldsMap.get(t) ?? []));
153
+ uniqueFields[memberType] = ownFields.filter((f) => !otherFields.has(f));
154
+ }
155
+ const prefixedUnionName = `${documentName}_${unionName}`;
156
+ resolvers[prefixedUnionName] = {
157
+ __resolveType: (obj) => {
158
+ for (const memberType of memberTypes) {
159
+ const fields = uniqueFields[memberType] ?? [];
160
+ if (fields.length > 0 && fields.some((f) => f in obj)) {
161
+ return `${documentName}_${memberType}`;
21
162
  }
22
- if (driveId) {
23
- const docIds = await reactor.getDocuments(driveId);
24
- if (!docIds.includes(docId)) {
25
- throw new Error(`Document with id ${docId} is not part of ${driveId}`);
163
+ }
164
+ return `${documentName}_${memberTypes[0]}`;
165
+ },
166
+ };
167
+ }
168
+ return resolvers;
169
+ }
170
+ /**
171
+ * Generate resolvers for this document model using reactorClient
172
+ * Uses flat queries (not nested) consistent with ReactorSubgraph patterns
173
+ */
174
+ generateResolvers() {
175
+ const documentType = this.documentModel.documentModel.global.id;
176
+ const documentName = getDocumentModelSchemaName(this.documentModel.documentModel.global);
177
+ const operations = this.documentModel.documentModel.global.specifications
178
+ .at(-1)
179
+ ?.modules.flatMap((module) => module.operations.filter((op) => op.name)) ?? [];
180
+ return {
181
+ ...this.generateUnionResolvers(),
182
+ Query: {
183
+ // Flat query: Get a specific document by identifier
184
+ // Uses shared documentResolver from reactor/resolvers.ts
185
+ [`${documentName}_document`]: async (_, args, ctx) => {
186
+ const { identifier, view } = args;
187
+ if (!identifier) {
188
+ throw new GraphQLError("Document identifier is required");
189
+ }
190
+ // Use shared resolver function
191
+ const result = await documentResolver(this.reactorClient, {
192
+ identifier,
193
+ view,
194
+ });
195
+ // Validate document type
196
+ if (result.document.documentType !== documentType) {
197
+ throw new GraphQLError(`Document with id ${identifier} is not of type ${documentType}`);
198
+ }
199
+ // Check permissions
200
+ await this.assertCanRead(result.document.id, ctx);
201
+ // Return shared resolver result directly (matches PHDocument format)
202
+ return result;
203
+ },
204
+ // Flat query: Find documents by search criteria (type is built-in)
205
+ // Uses shared findDocumentsResolver from reactor/resolvers.ts
206
+ [`${documentName}_findDocuments`]: async (_, args, ctx) => {
207
+ const { search, view, paging } = args;
208
+ // Use shared resolver function with built-in type filter
209
+ const result = await findDocumentsResolver(this.reactorClient, {
210
+ search: {
211
+ type: documentType, // Type is built-in for this document model subgraph
212
+ parentId: search.parentId,
213
+ },
214
+ view,
215
+ paging,
216
+ });
217
+ // Filter by permission if needed
218
+ if (!this.hasGlobalReadAccess(ctx) &&
219
+ this.documentPermissionService) {
220
+ const filteredItems = [];
221
+ for (const item of result.items) {
222
+ const canRead = await this.canReadDocument(item.id, ctx);
223
+ if (canRead) {
224
+ filteredItems.push(item);
26
225
  }
27
226
  }
28
- const doc = await reactor.getDocument(docId);
29
- if (doc.header.documentType !== documentType) {
30
- throw new Error(`Document with id ${docId} is not of type ${documentType}`);
31
- }
32
227
  return {
33
- driveId: driveId,
34
- ...buildGraphQlDocument(doc),
228
+ ...result,
229
+ items: filteredItems,
230
+ totalCount: filteredItems.length,
35
231
  };
36
- },
37
- getDocuments: async (args) => {
38
- const { driveId } = args;
39
- const docsIds = await reactor.getDocuments(driveId);
40
- const docs = await Promise.all(docsIds.map(async (docId) => {
41
- const doc = await reactor.getDocument(docId);
42
- return {
43
- driveId: driveId,
44
- ...buildGraphQlDocument(doc),
45
- };
46
- }));
47
- return docs.filter((doc) => doc.documentType === documentType);
48
- },
49
- };
50
- },
51
- },
52
- Mutation: {
53
- [`${documentName}_createDocument`]: async (_, args) => {
54
- const { driveId, name } = args;
55
- const document = await reactor.addDocument(documentType);
56
- if (driveId) {
57
- await reactor.addAction(driveId, addFile({
58
- name,
59
- id: document.header.id,
60
- documentType: documentType,
61
- }));
62
- }
63
- if (name) {
64
- await reactor.addAction(document.header.id, setName(name));
65
- }
66
- return document.header.id;
232
+ }
233
+ // Return shared resolver result directly (matches PHDocument format)
234
+ return result;
235
+ },
236
+ // Flat query: Get children of a document (filtered by this document type)
237
+ // Uses shared documentChildrenResolver from reactor/resolvers.ts
238
+ [`${documentName}_documentChildren`]: async (_, args, ctx) => {
239
+ const { parentIdentifier, view, paging } = args;
240
+ await this.assertCanRead(parentIdentifier, ctx);
241
+ // Use shared resolver function
242
+ const result = await documentChildrenResolver(this.reactorClient, {
243
+ parentIdentifier,
244
+ view,
245
+ paging,
246
+ });
247
+ // Filter children by this document type
248
+ const filteredItems = result.items.filter((item) => item.documentType === documentType);
249
+ return {
250
+ ...result,
251
+ items: filteredItems,
252
+ totalCount: filteredItems.length,
253
+ };
254
+ },
255
+ // Flat query: Get parents of a document
256
+ // Uses shared documentParentsResolver from reactor/resolvers.ts
257
+ [`${documentName}_documentParents`]: async (_, args, ctx) => {
258
+ const { childIdentifier, view, paging } = args;
259
+ await this.assertCanRead(childIdentifier, ctx);
260
+ // Use shared resolver function - return directly
261
+ return documentParentsResolver(this.reactorClient, {
262
+ childIdentifier,
263
+ view,
264
+ paging,
265
+ });
266
+ },
67
267
  },
68
- ...operations.reduce((mutations, op) => {
69
- mutations[`${documentName}_${camelCase(op.name)}`] = async (_, args) => {
70
- const { docId, input } = args;
71
- const doc = await reactor.getDocument(docId);
72
- if (!doc) {
73
- throw new Error("Document not found");
268
+ Mutation: {
269
+ // Uses shared createEmptyDocumentResolver from reactor/resolvers.ts
270
+ [`${documentName}_createDocument`]: async (_, args, ctx) => {
271
+ const { parentIdentifier, name } = args;
272
+ if (parentIdentifier) {
273
+ await this.assertCanWrite(parentIdentifier, ctx);
274
+ }
275
+ else if (!this.hasGlobalWriteAccess(ctx)) {
276
+ throw new GraphQLError("Forbidden: insufficient permissions to create documents");
74
277
  }
75
- const action = documentModel.actions[camelCase(op.name)];
76
- if (!action) {
77
- throw new Error(`Action ${op.name} not found`);
278
+ // Use shared resolver function - returns PHDocument format
279
+ const createdDoc = await createEmptyDocumentResolver(this.reactorClient, {
280
+ documentType,
281
+ parentIdentifier,
282
+ });
283
+ if (name) {
284
+ const updatedDoc = await this.reactorClient.execute(createdDoc.id, "main", [setName(name)]);
285
+ // Use toGqlPhDocument for PHDocument format with revisionsList
286
+ return toGqlPhDocument(updatedDoc);
78
287
  }
79
- const result = await reactor.addAction(docId, action(input));
80
- if (result.status !== "SUCCESS") {
81
- throw new Error(result.error?.message ?? `Failed to ${op.name}`);
288
+ // Return directly - already in PHDocument format
289
+ return createdDoc;
290
+ },
291
+ // Uses shared createEmptyDocumentResolver from reactor/resolvers.ts
292
+ [`${documentName}_createEmptyDocument`]: async (_, args, ctx) => {
293
+ const { parentIdentifier } = args;
294
+ if (parentIdentifier) {
295
+ await this.assertCanWrite(parentIdentifier, ctx);
82
296
  }
83
- const errorOp = result.operations.find((op) => op.error);
84
- if (errorOp) {
85
- throw new Error(errorOp.error);
297
+ else if (!this.hasGlobalWriteAccess(ctx)) {
298
+ throw new GraphQLError("Forbidden: insufficient permissions to create documents");
86
299
  }
87
- return result.operations.at(-1)?.index ?? -1;
88
- };
89
- return mutations;
90
- }, {}),
91
- },
92
- };
300
+ // Use shared resolver function - returns PHDocument format directly
301
+ return createEmptyDocumentResolver(this.reactorClient, {
302
+ documentType,
303
+ parentIdentifier,
304
+ });
305
+ },
306
+ // Generate sync and async mutations for each operation
307
+ ...operations.reduce((mutations, op) => {
308
+ // Sync mutation
309
+ mutations[`${documentName}_${camelCase(op.name)}`] = async (_, args, ctx) => {
310
+ const { docId, input } = args;
311
+ await this.assertCanWrite(docId, ctx);
312
+ await this.assertCanExecuteOperation(docId, op.name, ctx);
313
+ const doc = await this.reactorClient.get(docId);
314
+ if (doc.header.documentType !== documentType) {
315
+ throw new GraphQLError(`Document with id ${docId} is not of type ${documentType}`);
316
+ }
317
+ const action = this.documentModel.actions[camelCase(op.name)];
318
+ if (!action) {
319
+ throw new GraphQLError(`Action ${op.name} not found`);
320
+ }
321
+ try {
322
+ const updatedDoc = await this.reactorClient.execute(docId, "main", [action(input)]);
323
+ // Use toGqlPhDocument for PHDocument format with revisionsList
324
+ return toGqlPhDocument(updatedDoc);
325
+ }
326
+ catch (error) {
327
+ throw new GraphQLError(error instanceof Error
328
+ ? error.message
329
+ : `Failed to ${op.name}`);
330
+ }
331
+ };
332
+ // Async mutation - returns job ID
333
+ mutations[`${documentName}_${camelCase(op.name)}Async`] = async (_, args, ctx) => {
334
+ const { docId, input } = args;
335
+ await this.assertCanWrite(docId, ctx);
336
+ await this.assertCanExecuteOperation(docId, op.name, ctx);
337
+ const doc = await this.reactorClient.get(docId);
338
+ if (doc.header.documentType !== documentType) {
339
+ throw new GraphQLError(`Document with id ${docId} is not of type ${documentType}`);
340
+ }
341
+ const action = this.documentModel.actions[camelCase(op.name)];
342
+ if (!action) {
343
+ throw new GraphQLError(`Action ${op.name} not found`);
344
+ }
345
+ try {
346
+ const jobInfo = await this.reactorClient.executeAsync(docId, "main", [action(input)]);
347
+ return jobInfo.id;
348
+ }
349
+ catch (error) {
350
+ throw new GraphQLError(error instanceof Error
351
+ ? error.message
352
+ : `Failed to ${op.name}`);
353
+ }
354
+ };
355
+ return mutations;
356
+ }, {}),
357
+ },
358
+ };
359
+ }
93
360
  }
361
+ /**
362
+ * @deprecated Use `DocumentModelSubgraph` instead. This class uses the legacy `reactor` (IDocumentDriveServer)
363
+ * interface. The new `DocumentModelSubgraph` class uses `reactorClient` (IReactorClient) which provides
364
+ * better patterns and more capabilities. Enable via `useNewDocumentModelSubgraph: true` in GraphQLManager.
365
+ */
94
366
  export class DocumentModelSubgraphLegacy extends BaseSubgraph {
95
367
  documentModel;
96
368
  constructor(documentModel, args) {
97
369
  super(args);
98
370
  this.documentModel = documentModel;
99
371
  this.name = kebabCase(documentModel.documentModel.global.name);
100
- this.typeDefs = generateDocumentModelSchemaLegacy(this.documentModel.documentModel.global);
101
- this.resolvers = generateDocumentModelResolversLegacy(this.documentModel, args.reactor);
372
+ this.typeDefs = generateDocumentModelSchema(this.documentModel.documentModel.global);
373
+ this.resolvers = this.generateResolvers();
374
+ }
375
+ /**
376
+ * Check if user has global read access (admin, user, or guest)
377
+ */
378
+ hasGlobalReadAccess(ctx) {
379
+ const isGlobalAdmin = ctx.isAdmin?.(ctx.user?.address ?? "");
380
+ const isGlobalUser = ctx.isUser?.(ctx.user?.address ?? "");
381
+ const isGlobalGuest = ctx.isGuest?.(ctx.user?.address ?? "") ||
382
+ process.env.FREE_ENTRY === "true";
383
+ return !!(isGlobalAdmin || isGlobalUser || isGlobalGuest);
384
+ }
385
+ /**
386
+ * Check if user has global write access (admin or user, not guest)
387
+ */
388
+ hasGlobalWriteAccess(ctx) {
389
+ const isGlobalAdmin = ctx.isAdmin?.(ctx.user?.address ?? "");
390
+ const isGlobalUser = ctx.isUser?.(ctx.user?.address ?? "");
391
+ return !!(isGlobalAdmin || isGlobalUser);
392
+ }
393
+ /**
394
+ * Get the parent IDs function for hierarchical permission checks
395
+ */
396
+ getParentIdsFn() {
397
+ return async (documentId) => {
398
+ try {
399
+ const result = await this.reactorClient.getParents(documentId);
400
+ return result.results.map((doc) => doc.header.id);
401
+ }
402
+ catch {
403
+ return [];
404
+ }
405
+ };
406
+ }
407
+ /**
408
+ * Check if user can read a document (with hierarchy)
409
+ */
410
+ async canReadDocument(documentId, ctx) {
411
+ // Global access allows reading
412
+ if (this.hasGlobalReadAccess(ctx)) {
413
+ return true;
414
+ }
415
+ // Check document-level permissions with hierarchy
416
+ if (this.documentPermissionService) {
417
+ return this.documentPermissionService.canRead(documentId, ctx.user?.address, this.getParentIdsFn());
418
+ }
419
+ return false;
420
+ }
421
+ /**
422
+ * Check if user can write to a document (with hierarchy)
423
+ */
424
+ async canWriteDocument(documentId, ctx) {
425
+ // Global write access allows writing
426
+ if (this.hasGlobalWriteAccess(ctx)) {
427
+ return true;
428
+ }
429
+ // Check document-level permissions with hierarchy
430
+ if (this.documentPermissionService) {
431
+ return this.documentPermissionService.canWrite(documentId, ctx.user?.address, this.getParentIdsFn());
432
+ }
433
+ return false;
434
+ }
435
+ /**
436
+ * Throw an error if user cannot read the document
437
+ */
438
+ async assertCanRead(documentId, ctx) {
439
+ const canRead = await this.canReadDocument(documentId, ctx);
440
+ if (!canRead) {
441
+ throw new GraphQLError("Forbidden: insufficient permissions to read this document");
442
+ }
443
+ }
444
+ /**
445
+ * Throw an error if user cannot write to the document
446
+ */
447
+ async assertCanWrite(documentId, ctx) {
448
+ const canWrite = await this.canWriteDocument(documentId, ctx);
449
+ if (!canWrite) {
450
+ throw new GraphQLError("Forbidden: insufficient permissions to write to this document");
451
+ }
452
+ }
453
+ /**
454
+ * Check if user can execute a specific operation on a document.
455
+ * Throws an error if the operation is restricted and user lacks permission.
456
+ */
457
+ async assertCanExecuteOperation(documentId, operationType, ctx) {
458
+ // Skip if no permission service
459
+ if (!this.documentPermissionService) {
460
+ return;
461
+ }
462
+ // Global admins bypass operation-level restrictions
463
+ if (ctx.isAdmin?.(ctx.user?.address ?? "")) {
464
+ return;
465
+ }
466
+ // Check if this operation has any restrictions set
467
+ const isRestricted = await this.documentPermissionService.isOperationRestricted(documentId, operationType);
468
+ if (isRestricted) {
469
+ // Operation is restricted, check if user has permission
470
+ const canExecute = await this.documentPermissionService.canExecuteOperation(documentId, operationType, ctx.user?.address);
471
+ if (!canExecute) {
472
+ throw new GraphQLError(`Forbidden: insufficient permissions to execute operation "${operationType}" on this document`);
473
+ }
474
+ }
475
+ }
476
+ /**
477
+ * Generate resolvers for this document model with permission checks
478
+ */
479
+ generateResolvers() {
480
+ const documentType = this.documentModel.documentModel.global.id;
481
+ const documentName = getDocumentModelSchemaName(this.documentModel.documentModel.global);
482
+ const operations = this.documentModel.documentModel.global.specifications
483
+ .at(-1)
484
+ ?.modules.flatMap((module) => module.operations.filter((op) => op.name)) ?? [];
485
+ return {
486
+ Query: {
487
+ [documentName]: (_, __, ctx) => {
488
+ return {
489
+ getDocument: async (args) => {
490
+ const { docId, driveId } = args;
491
+ if (!docId) {
492
+ throw new Error("Document id is required");
493
+ }
494
+ // Check read permission before accessing document
495
+ await this.assertCanRead(docId, ctx);
496
+ if (driveId) {
497
+ const docIds = await this.reactor.getDocuments(driveId);
498
+ if (!docIds.includes(docId)) {
499
+ throw new Error(`Document with id ${docId} is not part of ${driveId}`);
500
+ }
501
+ }
502
+ const doc = await this.reactor.getDocument(docId);
503
+ if (doc.header.documentType !== documentType) {
504
+ throw new Error(`Document with id ${docId} is not of type ${documentType}`);
505
+ }
506
+ return {
507
+ driveId: driveId,
508
+ ...buildGraphQlDocument(doc),
509
+ };
510
+ },
511
+ getDocuments: async (args) => {
512
+ const { driveId } = args;
513
+ // Check read permission on drive before listing documents
514
+ await this.assertCanRead(driveId, ctx);
515
+ const docsIds = await this.reactor.getDocuments(driveId);
516
+ const docs = await Promise.all(docsIds.map(async (docId) => {
517
+ const doc = await this.reactor.getDocument(docId);
518
+ return {
519
+ driveId: driveId,
520
+ ...buildGraphQlDocument(doc),
521
+ };
522
+ }));
523
+ const filteredByType = docs.filter((doc) => doc.documentType === documentType);
524
+ // If user doesn't have global read access, filter by document-level permissions
525
+ if (!this.hasGlobalReadAccess(ctx) &&
526
+ this.documentPermissionService) {
527
+ const filteredDocs = [];
528
+ for (const doc of filteredByType) {
529
+ const canRead = await this.canReadDocument(doc.id, ctx);
530
+ if (canRead) {
531
+ filteredDocs.push(doc);
532
+ }
533
+ }
534
+ return filteredDocs;
535
+ }
536
+ return filteredByType;
537
+ },
538
+ };
539
+ },
540
+ },
541
+ Mutation: {
542
+ [`${documentName}_createDocument`]: async (_, args, ctx) => {
543
+ const { driveId, name } = args;
544
+ // If creating under a drive, check write permission on drive
545
+ if (driveId) {
546
+ await this.assertCanWrite(driveId, ctx);
547
+ }
548
+ else if (!this.hasGlobalWriteAccess(ctx)) {
549
+ throw new GraphQLError("Forbidden: insufficient permissions to create documents");
550
+ }
551
+ const document = await this.reactor.addDocument(documentType);
552
+ if (driveId) {
553
+ await this.reactor.addAction(driveId, addFile({
554
+ name,
555
+ id: document.header.id,
556
+ documentType: documentType,
557
+ }));
558
+ }
559
+ if (name) {
560
+ await this.reactor.addAction(document.header.id, setName(name));
561
+ }
562
+ return document.header.id;
563
+ },
564
+ ...operations.reduce((mutations, op) => {
565
+ mutations[`${documentName}_${camelCase(op.name)}`] = async (_, args, ctx) => {
566
+ const { docId, input } = args;
567
+ // Check write permission before mutating document
568
+ await this.assertCanWrite(docId, ctx);
569
+ // Check operation-level permissions
570
+ await this.assertCanExecuteOperation(docId, op.name, ctx);
571
+ const doc = await this.reactor.getDocument(docId);
572
+ if (!doc) {
573
+ throw new Error("Document not found");
574
+ }
575
+ const action = this.documentModel.actions[camelCase(op.name)];
576
+ if (!action) {
577
+ throw new Error(`Action ${op.name} not found`);
578
+ }
579
+ const result = await this.reactor.addAction(docId, action(input));
580
+ if (result.status !== "SUCCESS") {
581
+ throw new Error(result.error?.message ?? `Failed to ${op.name}`);
582
+ }
583
+ const errorOp = result.operations.find((op) => op.error);
584
+ if (errorOp) {
585
+ throw new Error(errorOp.error);
586
+ }
587
+ return result.operations.at(-1)?.index ?? -1;
588
+ };
589
+ return mutations;
590
+ }, {}),
591
+ },
592
+ };
102
593
  }
103
594
  }
104
595
  //# sourceMappingURL=document-model-subgraph.js.map