Adapters
chemistry
biology
finance
legal
art
climate
agent
Merge
BACCHUS45 commited on
Commit
aa67905
·
verified ·
1 Parent(s): 86eab3d

Upload integral_apollo_server.js

Browse files
Files changed (1) hide show
  1. integral_apollo_server.js +541 -0
integral_apollo_server.js ADDED
@@ -0,0 +1,541 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Integral API — Apollo Server implementation (MVP)
2
+ // This document contains multiple files you can copy into a project.
3
+
4
+ /*
5
+ File structure (paste into repo):
6
+
7
+ integral-apollo-server/
8
+ ├─ package.json
9
+ ├─ .env.example
10
+ ├─ docker-compose.yml
11
+ ├─ migrations/01_schema.sql
12
+ ├─ src/
13
+ | ├─ index.js -> Apollo server entry
14
+ | ├─ db.js -> Postgres pool
15
+ | ├─ pubsub.js -> Redis-backed PubSub
16
+ | ├─ schema.js -> GraphQL typeDefs
17
+ | └─ resolvers.js -> resolvers wired to Postgres + PubSub
18
+ └─ README.md
19
+
20
+ Follow README to run.
21
+ */
22
+
23
+ // -------------------------
24
+ // package.json
25
+ // -------------------------
26
+
27
+ /*
28
+ {
29
+ "name": "integral-apollo-server",
30
+ "version": "0.1.0",
31
+ "main": "src/index.js",
32
+ "scripts": {
33
+ "start": "node src/index.js",
34
+ "dev": "nodemon --watch src --exec node src/index.js"
35
+ },
36
+ "engines": { "node": ">=18" },
37
+ "dependencies": {
38
+ "apollo-server": "^3.11.1",
39
+ "graphql": "^16.6.0",
40
+ "pg": "^8.11.0",
41
+ "ioredis": "^5.3.2",
42
+ "graphql-redis-subscriptions": "^3.1.0",
43
+ "uuid": "^9.0.0",
44
+ "dotenv": "^16.0.3"
45
+ },
46
+ "devDependencies": {
47
+ "nodemon": "^2.0.22"
48
+ }
49
+ }
50
+ */
51
+
52
+ // -------------------------
53
+ // .env.example
54
+ // -------------------------
55
+
56
+ /*
57
+ # Postgres
58
+ DATABASE_URL=postgresql://integral:integral@postgres:5432/integral
59
+
60
+ # Redis
61
+ REDIS_URL=redis://redis:6379
62
+
63
+ # Server
64
+ PORT=4000
65
+
66
+ # JWT (if you add auth later)
67
+ JWT_SECRET=changeme
68
+ */
69
+
70
+ // -------------------------
71
+ // docker-compose.yml
72
+ // -------------------------
73
+
74
+ /*
75
+ version: '3.8'
76
+ services:
77
+ postgres:
78
+ image: postgres:15
79
+ environment:
80
+ POSTGRES_DB: integral
81
+ POSTGRES_USER: integral
82
+ POSTGRES_PASSWORD: integral
83
+ volumes:
84
+ - pgdata:/var/lib/postgresql/data
85
+ ports:
86
+ - '5432:5432'
87
+
88
+ redis:
89
+ image: redis:7
90
+ command: ["redis-server", "--save", "60", "1", "--appendonly", "yes"]
91
+ ports:
92
+ - '6379:6379'
93
+
94
+ app:
95
+ build: .
96
+ image: integral-apollo-server:dev
97
+ depends_on:
98
+ - postgres
99
+ - redis
100
+ environment:
101
+ - DATABASE_URL=${DATABASE_URL}
102
+ - REDIS_URL=${REDIS_URL}
103
+ - PORT=${PORT}
104
+ volumes:
105
+ - ./:/usr/src/app
106
+ command: ["npm", "run", "dev"]
107
+ ports:
108
+ - '4000:4000'
109
+
110
+ volumes:
111
+ pgdata:
112
+ */
113
+
114
+ // -------------------------
115
+ // migrations/01_schema.sql
116
+ // -------------------------
117
+
118
+ /*
119
+ -- migrations/01_schema.sql
120
+ CREATE EXTENSION IF NOT EXISTS pgcrypto;
121
+
122
+ CREATE TABLE IF NOT EXISTS objects (
123
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
124
+ namespace TEXT NOT NULL,
125
+ type TEXT NOT NULL,
126
+ timestamp TIMESTAMPTZ NOT NULL,
127
+ location JSONB,
128
+ severity INT,
129
+ confirmed BOOLEAN DEFAULT FALSE,
130
+ images JSONB,
131
+ provenance JSONB,
132
+ created_at TIMESTAMPTZ DEFAULT now()
133
+ );
134
+
135
+ CREATE TABLE IF NOT EXISTS payouts (
136
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
137
+ fault_id UUID REFERENCES objects(id) ON DELETE CASCADE,
138
+ amount_minor_units BIGINT NOT NULL,
139
+ currency VARCHAR(8) NOT NULL DEFAULT 'ZAR',
140
+ payee_id TEXT NOT NULL,
141
+ status VARCHAR(32) NOT NULL DEFAULT 'created',
142
+ tx_ref TEXT,
143
+ created_at TIMESTAMPTZ DEFAULT now(),
144
+ settled_at TIMESTAMPTZ
145
+ );
146
+
147
+ CREATE INDEX IF NOT EXISTS idx_objects_namespace ON objects(namespace);
148
+ CREATE INDEX IF NOT EXISTS idx_objects_confirmed ON objects(confirmed);
149
+ */
150
+
151
+ // -------------------------
152
+ // src/db.js
153
+ // -------------------------
154
+
155
+ /* src/db.js */
156
+ const { Pool } = require('pg');
157
+ require('dotenv').config();
158
+
159
+ const pool = new Pool({ connectionString: process.env.DATABASE_URL });
160
+
161
+ module.exports = {
162
+ query: (text, params) => pool.query(text, params),
163
+ pool
164
+ };
165
+
166
+ // -------------------------
167
+ // src/pubsub.js
168
+ // -------------------------
169
+
170
+ /* src/pubsub.js */
171
+ const { RedisPubSub } = require('graphql-redis-subscriptions');
172
+ const Redis = require('ioredis');
173
+ require('dotenv').config();
174
+
175
+ const options = {
176
+ retryStrategy: times => Math.min(times * 50, 2000),
177
+ };
178
+
179
+ const pubsub = new RedisPubSub({
180
+ publisher: new Redis(process.env.REDIS_URL, options),
181
+ subscriber: new Redis(process.env.REDIS_URL, options),
182
+ });
183
+
184
+ module.exports = pubsub;
185
+
186
+ // -------------------------
187
+ // src/schema.js
188
+ // -------------------------
189
+
190
+ /* src/schema.js */
191
+ const { gql } = require('apollo-server');
192
+
193
+ const typeDefs = gql`
194
+ scalar DateTime
195
+ scalar JSON
196
+
197
+ enum Namespace { infrastructure satellite }
198
+
199
+ type Location { lat: Float lon: Float }
200
+
201
+ type Provenance { source: String! license: String retrieved_at: DateTime! }
202
+
203
+ type InfrastructureFault {
204
+ id: ID!
205
+ namespace: Namespace!
206
+ type: String!
207
+ timestamp: DateTime!
208
+ location: JSON
209
+ severity: Int!
210
+ confirmed: Boolean!
211
+ images: [String]
212
+ provenance: Provenance
213
+ }
214
+
215
+ type Payout {
216
+ id: ID!
217
+ faultId: ID!
218
+ amountMinorUnits: Int!
219
+ currency: String!
220
+ payeeId: String!
221
+ status: String!
222
+ createdAt: DateTime!
223
+ settledAt: DateTime
224
+ txRef: String
225
+ }
226
+
227
+ input IngestFaultInput {
228
+ namespace: Namespace!
229
+ type: String!
230
+ timestamp: DateTime!
231
+ location: JSON
232
+ severity: Int!
233
+ images: [String]
234
+ provenance: JSON
235
+ }
236
+
237
+ input CreatePayoutInput {
238
+ faultId: ID!
239
+ amountMinorUnits: Int!
240
+ currency: String!
241
+ payeeId: String!
242
+ }
243
+
244
+ type Query {
245
+ listInfraFaults(limit: Int = 50, offset: Int = 0): [InfrastructureFault!]
246
+ payoutsForFault(faultId: ID!): [Payout!]
247
+ }
248
+
249
+ type Mutation {
250
+ ingestFault(input: IngestFaultInput!): InfrastructureFault!
251
+ confirmFault(id: ID!, confirmed: Boolean!): InfrastructureFault!
252
+ createPayout(input: CreatePayoutInput!): Payout!
253
+ settlePayout(payoutId: ID!): Payout!
254
+ }
255
+
256
+ type Subscription {
257
+ faultCreated: InfrastructureFault!
258
+ faultConfirmed: InfrastructureFault!
259
+ payoutUpdated: Payout!
260
+ }
261
+ `;
262
+
263
+ module.exports = typeDefs;
264
+
265
+ // -------------------------
266
+ // src/resolvers.js
267
+ // -------------------------
268
+
269
+ /* src/resolvers.js */
270
+ const { GraphQLScalarType, Kind } = require('graphql');
271
+ const db = require('./db');
272
+ const pubsub = require('./pubsub');
273
+ const { v4: uuidv4 } = require('uuid');
274
+
275
+ const FAULT_CREATED = 'FAULT_CREATED';
276
+ const FAULT_CONFIRMED = 'FAULT_CONFIRMED';
277
+ const PAYOUT_UPDATED = 'PAYOUT_UPDATED';
278
+
279
+ const DateTime = new GraphQLScalarType({
280
+ name: 'DateTime',
281
+ description: 'ISO date-time scalar',
282
+ parseValue: value => new Date(value),
283
+ serialize: value => value instanceof Date ? value.toISOString() : new Date(value).toISOString(),
284
+ parseLiteral(ast) {
285
+ if (ast.kind === Kind.STRING) return new Date(ast.value);
286
+ return null;
287
+ }
288
+ });
289
+
290
+ const JSONScalar = new GraphQLScalarType({
291
+ name: 'JSON',
292
+ description: 'Arbitrary JSON value',
293
+ parseValue: value => value,
294
+ serialize: value => value,
295
+ parseLiteral(ast) {
296
+ switch (ast.kind) {
297
+ case Kind.STRING: return ast.value;
298
+ case Kind.INT: return parseInt(ast.value, 10);
299
+ case Kind.FLOAT: return parseFloat(ast.value);
300
+ case Kind.BOOLEAN: return ast.value === 'true';
301
+ default: return null;
302
+ }
303
+ }
304
+ });
305
+
306
+ const resolvers = {
307
+ DateTime,
308
+ JSON: JSONScalar,
309
+
310
+ Query: {
311
+ listInfraFaults: async (_, { limit, offset }) => {
312
+ const r = await db.query('SELECT * FROM objects WHERE type=$1 ORDER BY created_at DESC LIMIT $2 OFFSET $3', ['pothole-detection', limit, offset]);
313
+ return r.rows.map(r => ({
314
+ id: r.id,
315
+ namespace: r.namespace,
316
+ type: r.type,
317
+ timestamp: r.timestamp,
318
+ location: r.location,
319
+ severity: r.severity,
320
+ confirmed: r.confirmed,
321
+ images: r.images,
322
+ provenance: r.provenance
323
+ }));
324
+ },
325
+ payoutsForFault: async (_, { faultId }) => {
326
+ const r = await db.query('SELECT * FROM payouts WHERE fault_id=$1 ORDER BY created_at DESC', [faultId]);
327
+ return r.rows.map(p => ({
328
+ id: p.id,
329
+ faultId: p.fault_id,
330
+ amountMinorUnits: parseInt(p.amount_minor_units, 10),
331
+ currency: p.currency,
332
+ payeeId: p.payee_id,
333
+ status: p.status,
334
+ createdAt: p.created_at,
335
+ settledAt: p.settled_at,
336
+ txRef: p.tx_ref
337
+ }));
338
+ }
339
+ },
340
+
341
+ Mutation: {
342
+ ingestFault: async (_, { input }) => {
343
+ const id = uuidv4();
344
+ const q = `INSERT INTO objects (id, namespace, type, timestamp, location, severity, images, provenance)
345
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING *`;
346
+ const vals = [id, input.namespace, input.type, input.timestamp || new Date().toISOString(), JSON.stringify(input.location || null), input.severity, JSON.stringify(input.images || []), input.provenance || {}];
347
+ const r = await db.query(q, vals);
348
+ const obj = r.rows[0];
349
+ const payload = {
350
+ id: obj.id,
351
+ namespace: obj.namespace,
352
+ type: obj.type,
353
+ timestamp: obj.timestamp,
354
+ location: obj.location,
355
+ severity: obj.severity,
356
+ confirmed: obj.confirmed,
357
+ images: obj.images,
358
+ provenance: obj.provenance
359
+ };
360
+ await pubsub.publish(FAULT_CREATED, payload);
361
+ return payload;
362
+ },
363
+
364
+ confirmFault: async (_, { id, confirmed }) => {
365
+ const q = 'UPDATE objects SET confirmed=$1 WHERE id=$2 RETURNING *';
366
+ const r = await db.query(q, [confirmed, id]);
367
+ if (r.rowCount === 0) throw new Error('fault not found');
368
+ const obj = r.rows[0];
369
+ const payload = {
370
+ id: obj.id,
371
+ namespace: obj.namespace,
372
+ type: obj.type,
373
+ timestamp: obj.timestamp,
374
+ location: obj.location,
375
+ severity: obj.severity,
376
+ confirmed: obj.confirmed,
377
+ images: obj.images,
378
+ provenance: obj.provenance
379
+ };
380
+ await pubsub.publish(FAULT_CONFIRMED, payload);
381
+ return payload;
382
+ },
383
+
384
+ createPayout: async (_, { input }) => {
385
+ const id = uuidv4();
386
+ const q = `INSERT INTO payouts (id, fault_id, amount_minor_units, currency, payee_id, status)
387
+ VALUES ($1,$2,$3,$4,$5,'created') RETURNING *`;
388
+ const vals = [id, input.faultId, input.amountMinorUnits, input.currency, input.payeeId];
389
+ const r = await db.query(q, vals);
390
+ const p = r.rows[0];
391
+ const payload = {
392
+ id: p.id,
393
+ faultId: p.fault_id,
394
+ amountMinorUnits: parseInt(p.amount_minor_units, 10),
395
+ currency: p.currency,
396
+ payeeId: p.payee_id,
397
+ status: p.status,
398
+ createdAt: p.created_at,
399
+ settledAt: p.settled_at,
400
+ txRef: p.tx_ref
401
+ };
402
+ await pubsub.publish(PAYOUT_UPDATED, payload);
403
+ return payload;
404
+ },
405
+
406
+ settlePayout: async (_, { payoutId }) => {
407
+ // naive settlement flow; mark processing -> settled, attach fake tx
408
+ const client = await db.pool.connect();
409
+ try {
410
+ await client.query('BEGIN');
411
+ const r = await client.query('SELECT * FROM payouts WHERE id=$1 FOR UPDATE', [payoutId]);
412
+ if (r.rowCount === 0) throw new Error('payout not found');
413
+ const payout = r.rows[0];
414
+ if (payout.status !== 'created' && payout.status !== 'processing') {
415
+ await client.query('ROLLBACK');
416
+ return {
417
+ id: payout.id,
418
+ faultId: payout.fault_id,
419
+ amountMinorUnits: parseInt(payout.amount_minor_units, 10),
420
+ currency: payout.currency,
421
+ payeeId: payout.payee_id,
422
+ status: payout.status,
423
+ createdAt: payout.created_at,
424
+ settledAt: payout.settled_at,
425
+ txRef: payout.tx_ref
426
+ };
427
+ }
428
+ await client.query('UPDATE payouts SET status=$1 WHERE id=$2', ['processing', payoutId]);
429
+ // call external payment gateway / TMS here — replaced by fake
430
+ const fakeTx = `TX-${Date.now()}-${Math.floor(Math.random()*1000)}`;
431
+ await client.query('UPDATE payouts SET status=$1, tx_ref=$2, settled_at=now() WHERE id=$3', ['settled', fakeTx, payoutId]);
432
+ await client.query('COMMIT');
433
+
434
+ const updated = (await db.query('SELECT * FROM payouts WHERE id=$1', [payoutId])).rows[0];
435
+ const payload = {
436
+ id: updated.id,
437
+ faultId: updated.fault_id,
438
+ amountMinorUnits: parseInt(updated.amount_minor_units, 10),
439
+ currency: updated.currency,
440
+ payeeId: updated.payee_id,
441
+ status: updated.status,
442
+ createdAt: updated.created_at,
443
+ settledAt: updated.settled_at,
444
+ txRef: updated.tx_ref
445
+ };
446
+ await pubsub.publish(PAYOUT_UPDATED, payload);
447
+ return payload;
448
+ } catch (err) {
449
+ await client.query('ROLLBACK');
450
+ throw err;
451
+ } finally {
452
+ client.release();
453
+ }
454
+ }
455
+ },
456
+
457
+ Subscription: {
458
+ faultCreated: { subscribe: () => pubsub.asyncIterator([FAULT_CREATED]) },
459
+ faultConfirmed: { subscribe: () => pubsub.asyncIterator([FAULT_CONFIRMED]) },
460
+ payoutUpdated: { subscribe: () => pubsub.asyncIterator([PAYOUT_UPDATED]) }
461
+ }
462
+ };
463
+
464
+ module.exports = resolvers;
465
+
466
+ // -------------------------
467
+ // src/index.js
468
+ // -------------------------
469
+
470
+ /* src/index.js */
471
+ const { ApolloServer } = require('apollo-server');
472
+ const typeDefs = require('./schema');
473
+ const resolvers = require('./resolvers');
474
+ require('dotenv').config();
475
+
476
+ async function start() {
477
+ const server = new ApolloServer({ typeDefs, resolvers });
478
+ const { url } = await server.listen({ port: process.env.PORT || 4000 });
479
+ console.log(`🚀 Server ready at ${url}`);
480
+ }
481
+
482
+ start();
483
+
484
+ // -------------------------
485
+ // README.md
486
+ // -------------------------
487
+
488
+ /* README.md
489
+ # Integral Apollo Server (MVP)
490
+
491
+ This repo is a minimal Apollo GraphQL server wired to Postgres + Redis as Pub/Sub.
492
+ It implements the "self-pay network" flows: ingest fault → confirm → create payout → settle payout.
493
+
494
+ ## Quickstart (Docker)
495
+ 1. Copy `.env.example` to `.env` and adjust if needed.
496
+ 2. Start services:
497
+
498
+ docker-compose up -d
499
+
500
+ 3. Run migrations into Postgres (example):
501
+
502
+ docker exec -i $(docker ps -q -f name=postgres) psql -U integral -d integral -f migrations/01_schema.sql
503
+
504
+ 4. Install dependencies and start app locally (or use the provided app container):
505
+
506
+ npm install
507
+ npm run dev
508
+
509
+ 5. Open GraphQL playground at http://localhost:4000/
510
+
511
+ ## Basic GraphQL examples
512
+ - Ingest fault (mutation) — will publish `faultCreated` subscription
513
+
514
+ mutation Ingest {
515
+ ingestFault(input:{ namespace: infrastructure, type: "pothole-detection", timestamp: "2025-11-30T18:00:00Z", severity: 2, location: {"lat": -29.12, "lon": 26.22} }) {
516
+ id
517
+ severity
518
+ confirmed
519
+ }
520
+ }
521
+
522
+ - Confirm fault (mutation) — will publish `faultConfirmed` and auto-create payout (in your custom flow if you wire it)
523
+
524
+ mutation Confirm { confirmFault(id: "<id>", confirmed: true) { id confirmed } }
525
+
526
+ - Create payout
527
+
528
+ mutation CreatePayout { createPayout(input:{ faultId: "<id>", amountMinorUnits: 5000, currency: "ZAR", payeeId: "local-contractor-123" }) { id status }
529
+ }
530
+
531
+ - Settle payout
532
+
533
+ mutation Settle { settlePayout(payoutId: "<payoutId>") { id status txRef }
534
+ }
535
+
536
+ ## Next steps
537
+ - Replace fake logic with ML verification service (image segmentation + sensor fusion)
538
+ - Integrate real TMS / payment gateway in `settlePayout`
539
+ - Add auth & RBAC for protected mutations
540
+ - Add audit trail table and signed provenance verification
541
+ */