@grafema/api 0.2.5-beta

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 (70) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +219 -0
  3. package/dist/context.d.ts +22 -0
  4. package/dist/context.d.ts.map +1 -0
  5. package/dist/context.js +18 -0
  6. package/dist/context.js.map +1 -0
  7. package/dist/dataloaders/index.d.ts +18 -0
  8. package/dist/dataloaders/index.d.ts.map +1 -0
  9. package/dist/dataloaders/index.js +17 -0
  10. package/dist/dataloaders/index.js.map +1 -0
  11. package/dist/dataloaders/nodeLoader.d.ts +19 -0
  12. package/dist/dataloaders/nodeLoader.d.ts.map +1 -0
  13. package/dist/dataloaders/nodeLoader.js +31 -0
  14. package/dist/dataloaders/nodeLoader.js.map +1 -0
  15. package/dist/index.d.ts +11 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +9 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/pagination.d.ts +50 -0
  20. package/dist/pagination.d.ts.map +1 -0
  21. package/dist/pagination.js +71 -0
  22. package/dist/pagination.js.map +1 -0
  23. package/dist/resolvers/edge.d.ts +22 -0
  24. package/dist/resolvers/edge.d.ts.map +1 -0
  25. package/dist/resolvers/edge.js +36 -0
  26. package/dist/resolvers/edge.js.map +1 -0
  27. package/dist/resolvers/index.d.ts +159 -0
  28. package/dist/resolvers/index.d.ts.map +1 -0
  29. package/dist/resolvers/index.js +21 -0
  30. package/dist/resolvers/index.js.map +1 -0
  31. package/dist/resolvers/mutation.d.ts +69 -0
  32. package/dist/resolvers/mutation.d.ts.map +1 -0
  33. package/dist/resolvers/mutation.js +82 -0
  34. package/dist/resolvers/mutation.js.map +1 -0
  35. package/dist/resolvers/node.d.ts +50 -0
  36. package/dist/resolvers/node.d.ts.map +1 -0
  37. package/dist/resolvers/node.js +69 -0
  38. package/dist/resolvers/node.js.map +1 -0
  39. package/dist/resolvers/query.d.ts +169 -0
  40. package/dist/resolvers/query.d.ts.map +1 -0
  41. package/dist/resolvers/query.js +188 -0
  42. package/dist/resolvers/query.js.map +1 -0
  43. package/dist/schema/enums.graphql +27 -0
  44. package/dist/schema/mutations.graphql +53 -0
  45. package/dist/schema/queries.graphql +213 -0
  46. package/dist/schema/scalars.graphql +2 -0
  47. package/dist/schema/subscriptions.graphql +84 -0
  48. package/dist/schema/types.graphql +440 -0
  49. package/dist/server.d.ts +31 -0
  50. package/dist/server.d.ts.map +1 -0
  51. package/dist/server.js +109 -0
  52. package/dist/server.js.map +1 -0
  53. package/package.json +51 -0
  54. package/src/context.ts +33 -0
  55. package/src/dataloaders/index.ts +24 -0
  56. package/src/dataloaders/nodeLoader.ts +41 -0
  57. package/src/index.ts +11 -0
  58. package/src/pagination.ts +109 -0
  59. package/src/resolvers/edge.ts +39 -0
  60. package/src/resolvers/index.ts +24 -0
  61. package/src/resolvers/mutation.ts +108 -0
  62. package/src/resolvers/node.ts +118 -0
  63. package/src/resolvers/query.ts +307 -0
  64. package/src/schema/enums.graphql +27 -0
  65. package/src/schema/mutations.graphql +53 -0
  66. package/src/schema/queries.graphql +213 -0
  67. package/src/schema/scalars.graphql +2 -0
  68. package/src/schema/subscriptions.graphql +84 -0
  69. package/src/schema/types.graphql +440 -0
  70. package/src/server.ts +140 -0
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@grafema/api",
3
+ "version": "0.2.5-beta",
4
+ "description": "GraphQL API server for Grafema code analysis toolkit",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "src"
17
+ ],
18
+ "keywords": [
19
+ "grafema",
20
+ "graphql",
21
+ "api",
22
+ "code-analysis"
23
+ ],
24
+ "license": "Apache-2.0",
25
+ "author": "Vadim Reshetnikov",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/Disentinel/grafema.git",
29
+ "directory": "packages/api"
30
+ },
31
+ "dependencies": {
32
+ "dataloader": "^2.2.3",
33
+ "graphql": "^16.10.0",
34
+ "graphql-scalars": "^1.24.0",
35
+ "graphql-yoga": "^5.10.11",
36
+ "@grafema/core": "0.2.5-beta",
37
+ "@grafema/types": "0.2.5-beta"
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "^25.0.8",
41
+ "tsx": "^4.19.2",
42
+ "typescript": "^5.9.3"
43
+ },
44
+ "scripts": {
45
+ "build": "tsc && pnpm run copy-schema",
46
+ "copy-schema": "mkdir -p dist/schema && cp src/schema/*.graphql dist/schema/",
47
+ "clean": "rm -rf dist",
48
+ "test": "node --import tsx --test test/*.test.ts",
49
+ "test:watch": "node --import tsx --test --watch test/*.test.ts"
50
+ }
51
+ }
package/src/context.ts ADDED
@@ -0,0 +1,33 @@
1
+ /**
2
+ * GraphQL Context
3
+ *
4
+ * Request-scoped context containing backend connection and DataLoaders.
5
+ */
6
+
7
+ import type { IncomingMessage } from 'node:http';
8
+ import type { RFDBServerBackend } from '@grafema/core';
9
+ import { createDataLoaders, type DataLoaders } from './dataloaders/index.js';
10
+
11
+ export interface GraphQLContext {
12
+ /** Graph backend connection */
13
+ backend: RFDBServerBackend;
14
+ /** DataLoaders for batching (per-request) */
15
+ loaders: DataLoaders;
16
+ /** Request start time for timeout tracking */
17
+ startTime: number;
18
+ }
19
+
20
+ /**
21
+ * Create context for a GraphQL request.
22
+ * Creates fresh DataLoaders to ensure no cross-request caching.
23
+ */
24
+ export function createContext(
25
+ backend: RFDBServerBackend,
26
+ _req: IncomingMessage
27
+ ): GraphQLContext {
28
+ return {
29
+ backend,
30
+ loaders: createDataLoaders(backend),
31
+ startTime: Date.now(),
32
+ };
33
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * DataLoader Factory
3
+ *
4
+ * Creates all DataLoaders for a request context.
5
+ * DataLoaders batch and cache database requests within a single GraphQL request.
6
+ */
7
+
8
+ import type { RFDBServerBackend } from '@grafema/core';
9
+ import { createNodeLoader } from './nodeLoader.js';
10
+
11
+ export interface DataLoaders {
12
+ /** Batch node lookups by ID */
13
+ node: ReturnType<typeof createNodeLoader>;
14
+ }
15
+
16
+ /**
17
+ * Create all DataLoaders for a request.
18
+ * DataLoaders are per-request to prevent cross-request caching issues.
19
+ */
20
+ export function createDataLoaders(backend: RFDBServerBackend): DataLoaders {
21
+ return {
22
+ node: createNodeLoader(backend),
23
+ };
24
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Node DataLoader
3
+ *
4
+ * Batches multiple getNode() calls into efficient parallel lookups.
5
+ * Critical for preventing N+1 queries in GraphQL.
6
+ */
7
+
8
+ import DataLoader from 'dataloader';
9
+ import type { BaseNodeRecord } from '@grafema/types';
10
+ import type { RFDBServerBackend } from '@grafema/core';
11
+
12
+ /**
13
+ * Create a DataLoader for batching node lookups.
14
+ *
15
+ * The loader batches all node ID requests made within a single tick
16
+ * and resolves them with parallel backend calls.
17
+ *
18
+ * Complexity: O(n) where n = unique node IDs requested
19
+ */
20
+ export function createNodeLoader(
21
+ backend: RFDBServerBackend
22
+ ): DataLoader<string, BaseNodeRecord | null> {
23
+ return new DataLoader<string, BaseNodeRecord | null>(
24
+ async (ids: readonly string[]) => {
25
+ // Parallelize individual lookups
26
+ // Future optimization: add batch getNodes() to RFDB protocol
27
+ const results = await Promise.all(
28
+ ids.map((id) => backend.getNode(id).catch(() => null))
29
+ );
30
+ return results;
31
+ },
32
+ {
33
+ // Cache results within this request
34
+ cache: true,
35
+ // Use identity function for cache key
36
+ cacheKeyFn: (id) => id,
37
+ // Max batch size to prevent overwhelming backend
38
+ maxBatchSize: 100,
39
+ }
40
+ );
41
+ }
package/src/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @grafema/api - GraphQL API for Grafema code graph
3
+ *
4
+ * Provides a GraphQL endpoint for querying the code graph.
5
+ * Supports cursor-based pagination, subscriptions for streaming,
6
+ * and Datalog query passthrough.
7
+ */
8
+
9
+ export { createGraphQLServer, startServer } from './server.js';
10
+ export type { GraphQLServerOptions } from './server.js';
11
+ export type { GraphQLContext } from './context.js';
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Cursor-based Pagination Utilities
3
+ *
4
+ * Implements Relay Connection spec for cursor-based pagination.
5
+ */
6
+
7
+ /**
8
+ * Encode a cursor from an ID.
9
+ * Format: base64("cursor:${id}")
10
+ */
11
+ export function encodeCursor(id: string): string {
12
+ return Buffer.from(`cursor:${id}`).toString('base64');
13
+ }
14
+
15
+ /**
16
+ * Decode a cursor to get the ID.
17
+ * Returns null if cursor is invalid.
18
+ */
19
+ export function decodeCursor(cursor: string): string | null {
20
+ try {
21
+ const decoded = Buffer.from(cursor, 'base64').toString('utf-8');
22
+ if (decoded.startsWith('cursor:')) {
23
+ return decoded.slice(7);
24
+ }
25
+ return null;
26
+ } catch {
27
+ return null;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * PageInfo structure per Relay spec.
33
+ */
34
+ export interface PageInfo {
35
+ hasNextPage: boolean;
36
+ hasPreviousPage: boolean;
37
+ startCursor: string | null;
38
+ endCursor: string | null;
39
+ }
40
+
41
+ /**
42
+ * Edge structure for connections.
43
+ */
44
+ export interface Edge<T> {
45
+ node: T;
46
+ cursor: string;
47
+ }
48
+
49
+ /**
50
+ * Connection structure per Relay spec.
51
+ */
52
+ export interface Connection<T> {
53
+ edges: Edge<T>[];
54
+ pageInfo: PageInfo;
55
+ totalCount: number;
56
+ }
57
+
58
+ /**
59
+ * Apply cursor-based pagination to an array.
60
+ *
61
+ * @param items - All items (already filtered)
62
+ * @param first - Number of items to return (default: 50, max: 250)
63
+ * @param after - Cursor to start after
64
+ * @param getId - Function to get ID from item for cursor encoding
65
+ * @returns Connection structure
66
+ */
67
+ export function paginateArray<T>(
68
+ items: T[],
69
+ first: number | null | undefined,
70
+ after: string | null | undefined,
71
+ getId: (item: T) => string
72
+ ): Connection<T> {
73
+ const limit = Math.min(first ?? 50, 250);
74
+
75
+ // Find start index based on cursor
76
+ let startIndex = 0;
77
+ if (after) {
78
+ const afterId = decodeCursor(after);
79
+ if (afterId) {
80
+ const afterIndex = items.findIndex((item) => getId(item) === afterId);
81
+ if (afterIndex !== -1) {
82
+ startIndex = afterIndex + 1;
83
+ }
84
+ }
85
+ }
86
+
87
+ // Slice items
88
+ const slicedItems = items.slice(startIndex, startIndex + limit);
89
+
90
+ // Build edges
91
+ const edges: Edge<T>[] = slicedItems.map((item) => ({
92
+ node: item,
93
+ cursor: encodeCursor(getId(item)),
94
+ }));
95
+
96
+ // Build pageInfo
97
+ const pageInfo: PageInfo = {
98
+ hasNextPage: startIndex + limit < items.length,
99
+ hasPreviousPage: startIndex > 0,
100
+ startCursor: edges.length > 0 ? edges[0].cursor : null,
101
+ endCursor: edges.length > 0 ? edges[edges.length - 1].cursor : null,
102
+ };
103
+
104
+ return {
105
+ edges,
106
+ pageInfo,
107
+ totalCount: items.length,
108
+ };
109
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Edge Type Resolvers
3
+ *
4
+ * Resolves fields on Edge type that require lookups.
5
+ */
6
+
7
+ import type { EdgeRecord } from '@grafema/types';
8
+ import type { GraphQLContext } from '../context.js';
9
+
10
+ export const edgeResolvers = {
11
+ /**
12
+ * Resolve source node.
13
+ */
14
+ async src(parent: EdgeRecord, _args: unknown, context: GraphQLContext) {
15
+ return context.loaders.node.load(parent.src);
16
+ },
17
+
18
+ /**
19
+ * Resolve destination node.
20
+ */
21
+ async dst(parent: EdgeRecord, _args: unknown, context: GraphQLContext) {
22
+ return context.loaders.node.load(parent.dst);
23
+ },
24
+
25
+ /**
26
+ * Resolve metadata field.
27
+ */
28
+ metadata(parent: EdgeRecord) {
29
+ if (!parent.metadata) return null;
30
+ if (typeof parent.metadata === 'string') {
31
+ try {
32
+ return JSON.parse(parent.metadata);
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+ return parent.metadata;
38
+ },
39
+ };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * GraphQL Resolver Map
3
+ *
4
+ * Combines all resolvers and adds custom scalar handlers.
5
+ */
6
+
7
+ import { JSONResolver } from 'graphql-scalars';
8
+ import { nodeResolvers } from './node.js';
9
+ import { edgeResolvers } from './edge.js';
10
+ import { queryResolvers } from './query.js';
11
+ import { mutationResolvers } from './mutation.js';
12
+
13
+ export const resolvers = {
14
+ // Custom scalars
15
+ JSON: JSONResolver,
16
+
17
+ // Type resolvers
18
+ Node: nodeResolvers,
19
+ Edge: edgeResolvers,
20
+
21
+ // Root resolvers
22
+ Query: queryResolvers,
23
+ Mutation: mutationResolvers,
24
+ };
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Mutation Resolvers
3
+ *
4
+ * Implements all Mutation type fields.
5
+ */
6
+
7
+ import type { GraphQLContext } from '../context.js';
8
+
9
+ export const mutationResolvers = {
10
+ /**
11
+ * Run project analysis.
12
+ * Placeholder - would integrate with Orchestrator.
13
+ */
14
+ async analyzeProject(
15
+ _: unknown,
16
+ _args: { service?: string | null; force?: boolean | null },
17
+ _context: GraphQLContext
18
+ ) {
19
+ // Placeholder - would integrate with Orchestrator
20
+ return {
21
+ success: false,
22
+ status: {
23
+ running: false,
24
+ phase: null,
25
+ message: 'Analysis via GraphQL not yet implemented',
26
+ servicesDiscovered: 0,
27
+ servicesAnalyzed: 0,
28
+ error: 'Not implemented',
29
+ },
30
+ };
31
+ },
32
+
33
+ /**
34
+ * Create a new guarantee.
35
+ * Placeholder - would integrate with GuaranteeManager.
36
+ */
37
+ async createGuarantee(
38
+ _: unknown,
39
+ _args: { input: Record<string, unknown> },
40
+ _context: GraphQLContext
41
+ ) {
42
+ throw new Error('createGuarantee not yet implemented');
43
+ },
44
+
45
+ /**
46
+ * Delete a guarantee.
47
+ * Placeholder.
48
+ */
49
+ async deleteGuarantee(
50
+ _: unknown,
51
+ _args: { name: string },
52
+ _context: GraphQLContext
53
+ ) {
54
+ throw new Error('deleteGuarantee not yet implemented');
55
+ },
56
+
57
+ /**
58
+ * Check guarantees.
59
+ * Placeholder.
60
+ */
61
+ async checkGuarantees(
62
+ _: unknown,
63
+ _args: { names?: string[] | null },
64
+ _context: GraphQLContext
65
+ ) {
66
+ return {
67
+ total: 0,
68
+ passed: 0,
69
+ failed: 0,
70
+ results: [],
71
+ };
72
+ },
73
+
74
+ /**
75
+ * Check ad-hoc invariant.
76
+ */
77
+ async checkInvariant(
78
+ _: unknown,
79
+ args: { rule: string; description?: string | null },
80
+ context: GraphQLContext
81
+ ) {
82
+ try {
83
+ const results = await context.backend.checkGuarantee(args.rule);
84
+ const passed = results.length === 0;
85
+
86
+ return {
87
+ guaranteeId: 'adhoc',
88
+ passed,
89
+ violationCount: results.length,
90
+ violations: results.slice(0, 10).map((_r) => {
91
+ // Would need async resolution to populate node data
92
+ return {
93
+ node: null,
94
+ file: null,
95
+ line: null,
96
+ };
97
+ }),
98
+ };
99
+ } catch {
100
+ return {
101
+ guaranteeId: 'adhoc',
102
+ passed: false,
103
+ violationCount: 0,
104
+ violations: [],
105
+ };
106
+ }
107
+ },
108
+ };
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Node Type Resolvers
3
+ *
4
+ * Resolves relationship fields on Node type.
5
+ */
6
+
7
+ import type { BaseNodeRecord, EdgeRecord } from '@grafema/types';
8
+ import type { GraphQLContext } from '../context.js';
9
+ import { paginateArray } from '../pagination.js';
10
+
11
+ export const nodeResolvers = {
12
+ /**
13
+ * Resolve outgoing edges with cursor-based pagination.
14
+ *
15
+ * Complexity: O(k) where k = number of outgoing edges from this node
16
+ */
17
+ async outgoingEdges(
18
+ parent: BaseNodeRecord,
19
+ args: { types?: string[] | null; first?: number | null; after?: string | null },
20
+ context: GraphQLContext
21
+ ) {
22
+ const edges = await context.backend.getOutgoingEdges(
23
+ parent.id,
24
+ args.types || null
25
+ );
26
+
27
+ return paginateArray(
28
+ edges,
29
+ args.first,
30
+ args.after,
31
+ (e: EdgeRecord) => `${e.src}:${e.dst}:${e.type}`
32
+ );
33
+ },
34
+
35
+ /**
36
+ * Resolve incoming edges with cursor-based pagination.
37
+ *
38
+ * Complexity: O(k) where k = number of incoming edges to this node
39
+ */
40
+ async incomingEdges(
41
+ parent: BaseNodeRecord,
42
+ args: { types?: string[] | null; first?: number | null; after?: string | null },
43
+ context: GraphQLContext
44
+ ) {
45
+ const edges = await context.backend.getIncomingEdges(
46
+ parent.id,
47
+ args.types || null
48
+ );
49
+
50
+ return paginateArray(
51
+ edges,
52
+ args.first,
53
+ args.after,
54
+ (e: EdgeRecord) => `${e.src}:${e.dst}:${e.type}`
55
+ );
56
+ },
57
+
58
+ /**
59
+ * Resolve child nodes (via CONTAINS edges) with cursor-based pagination.
60
+ *
61
+ * Complexity: O(c) where c = number of children
62
+ */
63
+ async children(
64
+ parent: BaseNodeRecord,
65
+ args: { first?: number | null; after?: string | null },
66
+ context: GraphQLContext
67
+ ) {
68
+ const edges = await context.backend.getOutgoingEdges(parent.id, ['CONTAINS']);
69
+
70
+ // Use DataLoader to batch child node lookups
71
+ const childIds = edges.map((e) => e.dst);
72
+ const children = await context.loaders.node.loadMany(childIds);
73
+
74
+ // Filter out errors and nulls
75
+ const validChildren = children.filter(
76
+ (c): c is BaseNodeRecord => c != null && !(c instanceof Error)
77
+ );
78
+
79
+ return paginateArray(
80
+ validChildren,
81
+ args.first,
82
+ args.after,
83
+ (n: BaseNodeRecord) => n.id
84
+ );
85
+ },
86
+
87
+ /**
88
+ * Resolve parent node (via incoming CONTAINS edge).
89
+ *
90
+ * Complexity: O(1) - single lookup
91
+ */
92
+ async parent(
93
+ parent: BaseNodeRecord,
94
+ _args: unknown,
95
+ context: GraphQLContext
96
+ ) {
97
+ const edges = await context.backend.getIncomingEdges(parent.id, ['CONTAINS']);
98
+ if (edges.length === 0) return null;
99
+
100
+ return context.loaders.node.load(edges[0].src);
101
+ },
102
+
103
+ /**
104
+ * Resolve metadata field.
105
+ * Parses JSON string if needed.
106
+ */
107
+ metadata(parent: BaseNodeRecord) {
108
+ if (!parent.metadata) return null;
109
+ if (typeof parent.metadata === 'string') {
110
+ try {
111
+ return JSON.parse(parent.metadata);
112
+ } catch {
113
+ return null;
114
+ }
115
+ }
116
+ return parent.metadata;
117
+ },
118
+ };