bunsane 0.1.1 → 0.1.2
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/core/App.ts +81 -32
- package/core/Entity.ts +34 -2
- package/core/Query.ts +57 -7
- package/core/RequestLoaders.ts +59 -35
- package/database/index.ts +5 -5
- package/gql/Generator.ts +2 -1
- package/gql/index.ts +11 -1
- package/package.json +16 -8
- package/tests/component.test.ts +134 -1
- package/examples/hooks/README.md +0 -228
- package/examples/hooks/audit-logger.ts +0 -495
- package/validate-docs.sh +0 -90
package/core/App.ts
CHANGED
|
@@ -140,55 +140,104 @@ export default class App {
|
|
|
140
140
|
private async handleRequest(req: Request): Promise<Response> {
|
|
141
141
|
const url = new URL(req.url);
|
|
142
142
|
const method = req.method;
|
|
143
|
+
const startTime = Date.now();
|
|
143
144
|
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
145
|
+
// Add request timeout
|
|
146
|
+
const controller = new AbortController();
|
|
147
|
+
const timeoutId = setTimeout(() => {
|
|
148
|
+
controller.abort();
|
|
149
|
+
logger.warn(`Request timeout: ${method} ${url.pathname}`);
|
|
150
|
+
}, 30000); // 30 second timeout
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
// Health check endpoint
|
|
154
|
+
if (url.pathname === '/health') {
|
|
155
|
+
clearTimeout(timeoutId);
|
|
156
|
+
return new Response(JSON.stringify({
|
|
157
|
+
status: 'ok',
|
|
158
|
+
timestamp: new Date().toISOString(),
|
|
159
|
+
uptime: process.uptime()
|
|
160
|
+
}), {
|
|
161
|
+
headers: { 'Content-Type': 'application/json' }
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
for (const [route, folder] of this.staticAssets) {
|
|
165
|
+
if (url.pathname.startsWith(route)) {
|
|
166
|
+
const relativePath = url.pathname.slice(route.length);
|
|
167
|
+
const filePath = path.join(folder, relativePath);
|
|
168
|
+
try {
|
|
169
|
+
const file = Bun.file(filePath);
|
|
170
|
+
if (await file.exists()) {
|
|
171
|
+
clearTimeout(timeoutId);
|
|
172
|
+
return new Response(file);
|
|
173
|
+
}
|
|
174
|
+
} catch (error) {
|
|
175
|
+
logger.error(`Error serving static file ${filePath}:`, error as any);
|
|
153
176
|
}
|
|
154
|
-
} catch (error) {
|
|
155
|
-
logger.error(`Error serving static file ${filePath}:`, error as any);
|
|
156
177
|
}
|
|
157
178
|
}
|
|
158
|
-
}
|
|
159
179
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
180
|
+
// Lookup REST endpoint using map for O(1) performance
|
|
181
|
+
const endpointKey = `${method}:${url.pathname}`;
|
|
182
|
+
const endpoint = this.restEndpointMap.get(endpointKey);
|
|
183
|
+
if (endpoint) {
|
|
184
|
+
try {
|
|
185
|
+
const result = await endpoint.handler(req);
|
|
186
|
+
const duration = Date.now() - startTime;
|
|
187
|
+
logger.trace(`REST ${method} ${url.pathname} completed in ${duration}ms`);
|
|
188
|
+
|
|
189
|
+
clearTimeout(timeoutId);
|
|
190
|
+
if (result instanceof Response) {
|
|
191
|
+
return result;
|
|
192
|
+
} else {
|
|
193
|
+
return new Response(JSON.stringify(result), {
|
|
194
|
+
headers: { 'Content-Type': 'application/json' }
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
} catch (error) {
|
|
198
|
+
const duration = Date.now() - startTime;
|
|
199
|
+
logger.error(`Error in REST endpoint ${method} ${endpoint.path} after ${duration}ms`, error as any);
|
|
200
|
+
clearTimeout(timeoutId);
|
|
201
|
+
return new Response(JSON.stringify({ error: 'Internal server error' }), {
|
|
202
|
+
status: 500,
|
|
170
203
|
headers: { 'Content-Type': 'application/json' }
|
|
171
204
|
});
|
|
172
205
|
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (this.yoga) {
|
|
209
|
+
const response = await this.yoga(req);
|
|
210
|
+
const duration = Date.now() - startTime;
|
|
211
|
+
logger.trace(`GraphQL request completed in ${duration}ms`);
|
|
212
|
+
clearTimeout(timeoutId);
|
|
213
|
+
return response;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
clearTimeout(timeoutId);
|
|
217
|
+
return new Response('Not Found', { status: 404 });
|
|
218
|
+
} catch (error) {
|
|
219
|
+
const duration = Date.now() - startTime;
|
|
220
|
+
logger.error(`Request failed after ${duration}ms: ${method} ${url.pathname}`, error as any);
|
|
221
|
+
clearTimeout(timeoutId);
|
|
222
|
+
|
|
223
|
+
if ((error as Error).name === 'AbortError') {
|
|
224
|
+
return new Response(JSON.stringify({ error: 'Request timeout' }), {
|
|
225
|
+
status: 408,
|
|
177
226
|
headers: { 'Content-Type': 'application/json' }
|
|
178
227
|
});
|
|
179
228
|
}
|
|
229
|
+
|
|
230
|
+
return new Response(JSON.stringify({ error: 'Internal server error' }), {
|
|
231
|
+
status: 500,
|
|
232
|
+
headers: { 'Content-Type': 'application/json' }
|
|
233
|
+
});
|
|
180
234
|
}
|
|
181
|
-
|
|
182
|
-
if (this.yoga) {
|
|
183
|
-
return this.yoga(req);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
return new Response('Not Found', { status: 404 });
|
|
187
235
|
}
|
|
188
236
|
|
|
189
237
|
async start() {
|
|
190
238
|
logger.info("Application Started");
|
|
191
239
|
const server = Bun.serve({
|
|
240
|
+
port: parseInt(process.env.PORT || "3000"),
|
|
192
241
|
fetch: this.handleRequest.bind(this),
|
|
193
242
|
});
|
|
194
243
|
logger.info(`Server is running on ${new URL(this.yoga?.graphqlEndpoint || '/graphql', `http://${server.hostname}:${server.port}`)}`)
|
package/core/Entity.ts
CHANGED
|
@@ -14,6 +14,7 @@ export class Entity {
|
|
|
14
14
|
id: string;
|
|
15
15
|
public _persisted: boolean = false;
|
|
16
16
|
private components: Map<string, BaseComponent> = new Map<string, BaseComponent>();
|
|
17
|
+
private removedComponents: Set<string> = new Set<string>();
|
|
17
18
|
protected _dirty: boolean = false;
|
|
18
19
|
|
|
19
20
|
constructor(id?: string) {
|
|
@@ -91,11 +92,17 @@ export class Entity {
|
|
|
91
92
|
/**
|
|
92
93
|
* Removes a component from the entity.
|
|
93
94
|
* Use like: entity.remove(Component)
|
|
95
|
+
* WARNING: This will delete the component from the database upon saving the entity.
|
|
96
|
+
* If you want to keep the component in the database but just remove it from the entity instance,
|
|
97
|
+
* consider implementing a different method.
|
|
94
98
|
*/
|
|
95
99
|
public remove<T extends BaseComponent>(ctor: new (...args: any[]) => T): boolean {
|
|
96
100
|
const component = Array.from(this.components.values()).find(comp => comp instanceof ctor) as T;
|
|
97
101
|
|
|
98
102
|
if (component) {
|
|
103
|
+
// Track the component type for database deletion
|
|
104
|
+
this.removedComponents.add(component.getTypeID());
|
|
105
|
+
|
|
99
106
|
// Remove the component from the map
|
|
100
107
|
this.components.delete(component.getTypeID());
|
|
101
108
|
this._dirty = true;
|
|
@@ -150,7 +157,23 @@ export class Entity {
|
|
|
150
157
|
|
|
151
158
|
@timed("Entity.save")
|
|
152
159
|
public save() {
|
|
153
|
-
return
|
|
160
|
+
return new Promise<boolean>((resolve, reject) => {
|
|
161
|
+
// Add timeout to prevent hanging
|
|
162
|
+
const timeout = setTimeout(() => {
|
|
163
|
+
logger.error(`Entity save timeout for entity ${this.id}`);
|
|
164
|
+
reject(new Error(`Entity save timeout for entity ${this.id}`));
|
|
165
|
+
}, 30000); // 30 second timeout
|
|
166
|
+
|
|
167
|
+
this.doSave()
|
|
168
|
+
.then(result => {
|
|
169
|
+
clearTimeout(timeout);
|
|
170
|
+
resolve(result);
|
|
171
|
+
})
|
|
172
|
+
.catch(error => {
|
|
173
|
+
clearTimeout(timeout);
|
|
174
|
+
reject(error);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
154
177
|
}
|
|
155
178
|
|
|
156
179
|
|
|
@@ -158,7 +181,7 @@ export class Entity {
|
|
|
158
181
|
public doSave() {
|
|
159
182
|
return new Promise<boolean>(async resolve => {
|
|
160
183
|
if(!this._dirty) {
|
|
161
|
-
|
|
184
|
+
logger.trace("Entity is not dirty, no need to save.");
|
|
162
185
|
return resolve(true);
|
|
163
186
|
}
|
|
164
187
|
|
|
@@ -170,6 +193,15 @@ export class Entity {
|
|
|
170
193
|
await trx`INSERT INTO entities (id) VALUES (${this.id}) ON CONFLICT DO NOTHING`;
|
|
171
194
|
this._persisted = true;
|
|
172
195
|
}
|
|
196
|
+
|
|
197
|
+
// Delete removed components from database
|
|
198
|
+
if (this.removedComponents.size > 0) {
|
|
199
|
+
const typeIds = Array.from(this.removedComponents);
|
|
200
|
+
await trx`DELETE FROM components WHERE entity_id = ${this.id} AND type_id IN ${sql(typeIds)}`;
|
|
201
|
+
await trx`DELETE FROM entity_components WHERE entity_id = ${this.id} AND type_id IN ${sql(typeIds)}`;
|
|
202
|
+
this.removedComponents.clear();
|
|
203
|
+
}
|
|
204
|
+
|
|
173
205
|
if(this.components.size === 0) {
|
|
174
206
|
logger.trace(`No components to save for entity ${this.id}`);
|
|
175
207
|
return;
|
package/core/Query.ts
CHANGED
|
@@ -117,6 +117,10 @@ class Query {
|
|
|
117
117
|
|
|
118
118
|
private buildFilterCondition(filter: QueryFilter, alias: string, paramIndex: number): { sql: string, params: any[], newParamIndex: number } {
|
|
119
119
|
const { field, operator, value } = filter;
|
|
120
|
+
|
|
121
|
+
// Build JSON path for nested properties (e.g., "parent.child" -> data->'parent'->>'child')
|
|
122
|
+
const jsonPath = this.buildJsonPath(field, alias);
|
|
123
|
+
|
|
120
124
|
switch (operator) {
|
|
121
125
|
case "=":
|
|
122
126
|
case ">":
|
|
@@ -125,22 +129,22 @@ class Query {
|
|
|
125
129
|
case "<=":
|
|
126
130
|
case "!=":
|
|
127
131
|
if (typeof value === "string") {
|
|
128
|
-
return { sql: `${
|
|
132
|
+
return { sql: `${jsonPath} ${operator} $${paramIndex}`, params: [value], newParamIndex: paramIndex + 1 };
|
|
129
133
|
} else {
|
|
130
|
-
return { sql: `(${
|
|
134
|
+
return { sql: `(${jsonPath})::numeric ${operator} $${paramIndex}`, params: [value], newParamIndex: paramIndex + 1 };
|
|
131
135
|
}
|
|
132
136
|
case "LIKE":
|
|
133
|
-
return { sql: `${
|
|
137
|
+
return { sql: `${jsonPath} LIKE $${paramIndex}`, params: [value], newParamIndex: paramIndex + 1 };
|
|
134
138
|
case "IN":
|
|
135
139
|
if (Array.isArray(value)) {
|
|
136
140
|
const placeholders = Array.from({length: value.length}, (_, i) => `$${paramIndex + i}`).join(', ');
|
|
137
|
-
return { sql: `${
|
|
141
|
+
return { sql: `${jsonPath} IN (${placeholders})`, params: value, newParamIndex: paramIndex + value.length };
|
|
138
142
|
}
|
|
139
143
|
throw new Error("IN operator requires an array of values");
|
|
140
144
|
case "NOT IN":
|
|
141
145
|
if (Array.isArray(value)) {
|
|
142
146
|
const placeholders = Array.from({length: value.length}, (_, i) => `$${paramIndex + i}`).join(', ');
|
|
143
|
-
return { sql: `${
|
|
147
|
+
return { sql: `${jsonPath} NOT IN (${placeholders})`, params: value, newParamIndex: paramIndex + value.length };
|
|
144
148
|
}
|
|
145
149
|
throw new Error("NOT IN operator requires an array of values");
|
|
146
150
|
default:
|
|
@@ -148,6 +152,27 @@ class Query {
|
|
|
148
152
|
}
|
|
149
153
|
}
|
|
150
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Build PostgreSQL JSON path expression for nested properties
|
|
157
|
+
* @param field Field path (e.g., "parent.child.grandchild")
|
|
158
|
+
* @param alias Table alias for the components table
|
|
159
|
+
* @returns PostgreSQL JSON path expression
|
|
160
|
+
*/
|
|
161
|
+
private buildJsonPath(field: string, alias: string): string {
|
|
162
|
+
const parts = field.split('.');
|
|
163
|
+
|
|
164
|
+
if (parts.length === 1) {
|
|
165
|
+
// Single level: data->>'field'
|
|
166
|
+
return `${alias}.data->>'${field}'`;
|
|
167
|
+
} else {
|
|
168
|
+
// Nested levels: data->'parent'->'child'->>'grandchild'
|
|
169
|
+
const pathParts = parts.slice(0, -1).map(part => `'${part}'`);
|
|
170
|
+
const lastPart = parts[parts.length - 1];
|
|
171
|
+
|
|
172
|
+
return `${alias}.data->${pathParts.join('->')}->>'${lastPart}'`;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
151
176
|
private buildFilterWhereClause(typeId: string, filters: QueryFilter[], alias: string, paramIndex: number): { sql: string, params: any[], newParamIndex: number } {
|
|
152
177
|
if (filters.length === 0) return { sql: '', params: [], newParamIndex: paramIndex };
|
|
153
178
|
|
|
@@ -235,6 +260,26 @@ class Query {
|
|
|
235
260
|
|
|
236
261
|
@timed("Query.exec")
|
|
237
262
|
public async exec(): Promise<Entity[]> {
|
|
263
|
+
return new Promise<Entity[]>((resolve, reject) => {
|
|
264
|
+
// Add timeout to prevent hanging queries
|
|
265
|
+
const timeout = setTimeout(() => {
|
|
266
|
+
logger.error(`Query execution timeout`);
|
|
267
|
+
reject(new Error(`Query execution timeout after 30 seconds`));
|
|
268
|
+
}, 30000); // 30 second timeout
|
|
269
|
+
|
|
270
|
+
this.doExec()
|
|
271
|
+
.then(result => {
|
|
272
|
+
clearTimeout(timeout);
|
|
273
|
+
resolve(result);
|
|
274
|
+
})
|
|
275
|
+
.catch(error => {
|
|
276
|
+
clearTimeout(timeout);
|
|
277
|
+
reject(error);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private async doExec(): Promise<Entity[]> {
|
|
238
283
|
const componentIds = Array.from(this.requiredComponents);
|
|
239
284
|
const excludedIds = Array.from(this.excludedComponents);
|
|
240
285
|
const componentCount = componentIds.length;
|
|
@@ -422,7 +467,10 @@ class Query {
|
|
|
422
467
|
const componentAlias = `c_${typeId}`;
|
|
423
468
|
const direction = order.direction.toUpperCase();
|
|
424
469
|
const nulls = order.nullsFirst ? 'NULLS FIRST' : 'NULLS LAST';
|
|
425
|
-
|
|
470
|
+
|
|
471
|
+
// Use buildJsonPath for nested property support
|
|
472
|
+
const jsonPath = this.buildJsonPath(order.property, componentAlias);
|
|
473
|
+
orderClauses.push(`(${jsonPath})::text ${direction} ${nulls}`);
|
|
426
474
|
}
|
|
427
475
|
|
|
428
476
|
// Always include entity_id as final tiebreaker for consistent ordering
|
|
@@ -475,7 +523,9 @@ class Query {
|
|
|
475
523
|
const direction = order.direction.toUpperCase();
|
|
476
524
|
const nullsClause = order.nullsFirst ? "NULLS FIRST" : "NULLS LAST";
|
|
477
525
|
|
|
478
|
-
|
|
526
|
+
// Use buildJsonPath for nested property support
|
|
527
|
+
const jsonPath = this.buildJsonPath(order.property, alias);
|
|
528
|
+
const selectExpr = `, (${jsonPath})::numeric as sort_val`;
|
|
479
529
|
const orderByExpr = `ORDER BY sort_val ${direction} ${nullsClause}, ec.entity_id ASC`;
|
|
480
530
|
|
|
481
531
|
return { select: selectExpr, orderBy: orderByExpr };
|
package/core/RequestLoaders.ts
CHANGED
|
@@ -18,46 +18,70 @@ export type RequestLoaders = {
|
|
|
18
18
|
|
|
19
19
|
export function createRequestLoaders(db: any): RequestLoaders {
|
|
20
20
|
const entityById = new DataLoader<string, Entity | null>(async (ids: readonly string[]) => {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
21
|
+
const startTime = Date.now();
|
|
22
|
+
try {
|
|
23
|
+
const uniqueIds = [...new Set(ids)];
|
|
24
|
+
const rows = await db`
|
|
25
|
+
SELECT id
|
|
26
|
+
FROM entities
|
|
27
|
+
WHERE id IN ${inList(uniqueIds, 1)}
|
|
28
|
+
AND deleted_at IS NULL
|
|
29
|
+
`;
|
|
30
|
+
const entities = rows.map((row: any) => {
|
|
31
|
+
const entity = new Entity(row.id);
|
|
32
|
+
entity.setPersisted(true);
|
|
33
|
+
return entity;
|
|
34
|
+
});
|
|
35
|
+
const map = new Map<string, Entity>();
|
|
36
|
+
entities.forEach((e: Entity) => map.set(e.id, e));
|
|
37
|
+
|
|
38
|
+
const duration = Date.now() - startTime;
|
|
39
|
+
if (duration > 1000) { // Log slow queries
|
|
40
|
+
console.warn(`Slow entityById query: ${duration}ms for ${ids.length} entities`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return ids.map(id => map.get(id) ?? null);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error(`Error in entityById DataLoader:`, error);
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
36
48
|
});
|
|
37
49
|
|
|
38
50
|
const componentsByEntityType = new DataLoader<{ entityId: string; typeId: number }, ComponentData | null>(
|
|
39
51
|
async (keys: readonly { entityId: string; typeId: number }[]) => {
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
52
|
+
const startTime = Date.now();
|
|
53
|
+
try {
|
|
54
|
+
const entityIds = [...new Set(keys.map(k => k.entityId))];
|
|
55
|
+
const typeIds = [...new Set(keys.map(k => k.typeId))];
|
|
56
|
+
const rows = await db`
|
|
57
|
+
SELECT entity_id, type_id, data, created_at, updated_at, deleted_at
|
|
58
|
+
FROM components
|
|
59
|
+
WHERE entity_id IN ${inList(entityIds, 1)}
|
|
60
|
+
AND type_id IN ${inList(typeIds, entityIds.length + 1)}
|
|
61
|
+
AND deleted_at IS NULL
|
|
62
|
+
`;
|
|
63
|
+
const map = new Map<string, ComponentData>();
|
|
64
|
+
rows.forEach((row: any) => {
|
|
65
|
+
const key = `${row.entity_id}-${row.type_id}`;
|
|
66
|
+
map.set(key, {
|
|
67
|
+
typeId: row.type_id,
|
|
68
|
+
data: row.data,
|
|
69
|
+
createdAt: row.created_at,
|
|
70
|
+
updatedAt: row.updated_at,
|
|
71
|
+
deletedAt: row.deleted_at,
|
|
72
|
+
});
|
|
58
73
|
});
|
|
59
|
-
|
|
60
|
-
|
|
74
|
+
|
|
75
|
+
const duration = Date.now() - startTime;
|
|
76
|
+
if (duration > 1000) { // Log slow queries
|
|
77
|
+
console.warn(`Slow componentsByEntityType query: ${duration}ms for ${keys.length} keys`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return keys.map(k => map.get(`${k.entityId}-${k.typeId}`) ?? null);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error(`Error in componentsByEntityType DataLoader:`, error);
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
61
85
|
}
|
|
62
86
|
);
|
|
63
87
|
|
package/database/index.ts
CHANGED
|
@@ -2,11 +2,11 @@ import {SQL} from "bun";
|
|
|
2
2
|
import { logger } from "core/Logger";
|
|
3
3
|
|
|
4
4
|
const db = new SQL({
|
|
5
|
-
url: `postgres://${process.env.POSTGRES_USER}:${process.env.POSTGRES_PASSWORD}@${process.env.POSTGRES_HOST}
|
|
6
|
-
// Connection pool settings
|
|
7
|
-
max: parseInt(process.env.POSTGRES_MAX_CONNECTIONS ?? '
|
|
8
|
-
idleTimeout:
|
|
9
|
-
maxLifetime:
|
|
5
|
+
url: `postgres://${process.env.POSTGRES_USER}:${process.env.POSTGRES_PASSWORD}@${process.env.POSTGRES_HOST}:${process.env.POSTGRES_PORT ?? "5432"}/${process.env.POSTGRES_DB}`,
|
|
6
|
+
// Connection pool settings - FIXED
|
|
7
|
+
max: parseInt(process.env.POSTGRES_MAX_CONNECTIONS ?? '20', 10), // Increased max connections
|
|
8
|
+
idleTimeout: 30000, // Close idle connections after 30s (was 0)
|
|
9
|
+
maxLifetime: 600000, // Connection lifetime 10 minutes (was 0 = forever)
|
|
10
10
|
connectionTimeout: 30, // Timeout when establishing new connections
|
|
11
11
|
onclose: (err) => {
|
|
12
12
|
if (err) {
|
package/gql/Generator.ts
CHANGED
|
@@ -57,7 +57,7 @@ export function generateGraphQLSchema(services: any[]): { schema: GraphQLSchema
|
|
|
57
57
|
let typeDefs = `
|
|
58
58
|
`;
|
|
59
59
|
const scalarTypes: Set<string> = new Set();
|
|
60
|
-
const resolvers: any = {
|
|
60
|
+
const resolvers: any = {};
|
|
61
61
|
const queryFields: string[] = [];
|
|
62
62
|
const mutationFields: string[] = [];
|
|
63
63
|
|
|
@@ -80,6 +80,7 @@ export function generateGraphQLSchema(services: any[]): { schema: GraphQLSchema
|
|
|
80
80
|
if (service.__graphqlOperations) {
|
|
81
81
|
service.__graphqlOperations.forEach((op: any) => {
|
|
82
82
|
const { type, name, input, output, propertyKey } = op;
|
|
83
|
+
if (!resolvers[type]) resolvers[type] = {};
|
|
83
84
|
let fieldDef = `${name}`;
|
|
84
85
|
if (input) {
|
|
85
86
|
const inputName = `${name}Input`;
|
package/gql/index.ts
CHANGED
|
@@ -71,9 +71,19 @@ const staticResolvers = {
|
|
|
71
71
|
};
|
|
72
72
|
|
|
73
73
|
const maskError = (error: any, message: string): GraphQLError => {
|
|
74
|
+
// Handle authentication errors
|
|
75
|
+
if (error.message === 'Unauthenticated' || error.extensions?.http?.status === 401 || error.extensions?.code === 'UNAUTHENTICATED') {
|
|
76
|
+
return new GraphQLError('Unauthorized', {
|
|
77
|
+
extensions: {
|
|
78
|
+
code: 'UNAUTHORIZED',
|
|
79
|
+
http: { status: 401 }
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
74
84
|
// Handle JWT authentication errors specifically
|
|
75
85
|
if (error.extensions?.code === 'DOWNSTREAM_SERVICE_ERROR' && error.extensions?.http?.status === 401) {
|
|
76
|
-
return new GraphQLError('
|
|
86
|
+
return new GraphQLError('Unauthorized', {
|
|
77
87
|
extensions: {
|
|
78
88
|
code: 'UNAUTHORIZED',
|
|
79
89
|
http: { status: 401 }
|
package/package.json
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bunsane",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"author": {
|
|
5
5
|
"name": "yaaruu"
|
|
6
6
|
},
|
|
7
|
+
"keywords": [
|
|
8
|
+
"bun",
|
|
9
|
+
"framework",
|
|
10
|
+
"entity-component-system",
|
|
11
|
+
"ecs",
|
|
12
|
+
"graphql",
|
|
13
|
+
"typescript"
|
|
14
|
+
],
|
|
7
15
|
"module": "index.ts",
|
|
8
16
|
"type": "module",
|
|
9
17
|
"devDependencies": {
|
|
@@ -13,13 +21,13 @@
|
|
|
13
21
|
"typescript": "^5"
|
|
14
22
|
},
|
|
15
23
|
"dependencies": {
|
|
16
|
-
"dataloader": "
|
|
17
|
-
"graphql": "
|
|
18
|
-
"graphql-yoga": "
|
|
19
|
-
"pino": "
|
|
20
|
-
"pino-pretty": "
|
|
21
|
-
"reflect-metadata": "
|
|
22
|
-
"zod": "
|
|
24
|
+
"dataloader": "2.2.2",
|
|
25
|
+
"graphql": "16.11.0",
|
|
26
|
+
"graphql-yoga": "5.15.1",
|
|
27
|
+
"pino": "9.9.0",
|
|
28
|
+
"pino-pretty": "13.1.1",
|
|
29
|
+
"reflect-metadata": "0.2.2",
|
|
30
|
+
"zod": "4.1.5"
|
|
23
31
|
},
|
|
24
32
|
"repository": {
|
|
25
33
|
"type": "git",
|