bunsane 0.1.4 → 0.2.0
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/.claude/settings.local.json +47 -0
- package/.claude/skills/update-memory.md +74 -0
- package/.prettierrc +4 -0
- package/.serena/memories/architectural-decision-no-dependency-injection.md +76 -0
- package/.serena/memories/architecture.md +154 -0
- package/.serena/memories/cache-interface-refactoring-2026-01-24.md +165 -0
- package/.serena/memories/code_style_and_conventions.md +76 -0
- package/.serena/memories/project_overview.md +43 -0
- package/.serena/memories/schema-dsl-plan.md +107 -0
- package/.serena/memories/suggested_commands.md +80 -0
- package/.serena/memories/typescript-compilation-status.md +54 -0
- package/.serena/project.yml +114 -0
- package/TODO.md +1 -7
- package/bun.lock +150 -4
- package/bunfig.toml +10 -0
- package/config/cache.config.ts +77 -0
- package/config/upload.config.ts +4 -5
- package/core/App.ts +870 -123
- package/core/ArcheType.ts +2268 -377
- package/core/BatchLoader.ts +181 -71
- package/core/Config.ts +153 -0
- package/core/Decorators.ts +4 -1
- package/core/Entity.ts +621 -92
- package/core/EntityHookManager.ts +1 -1
- package/core/EntityInterface.ts +3 -1
- package/core/EntityManager.ts +1 -13
- package/core/ErrorHandler.ts +8 -2
- package/core/Logger.ts +9 -0
- package/core/Middleware.ts +34 -0
- package/core/RequestContext.ts +5 -1
- package/core/RequestLoaders.ts +227 -93
- package/core/SchedulerManager.ts +193 -52
- package/core/cache/CacheAnalytics.ts +399 -0
- package/core/cache/CacheFactory.ts +145 -0
- package/core/cache/CacheManager.ts +520 -0
- package/core/cache/CacheProvider.ts +34 -0
- package/core/cache/CacheWarmer.ts +157 -0
- package/core/cache/CompressionUtils.ts +110 -0
- package/core/cache/MemoryCache.ts +251 -0
- package/core/cache/MultiLevelCache.ts +180 -0
- package/core/cache/NoOpCache.ts +53 -0
- package/core/cache/RedisCache.ts +464 -0
- package/core/cache/TTLStrategy.ts +254 -0
- package/core/cache/index.ts +6 -0
- package/core/components/BaseComponent.ts +120 -0
- package/core/{ComponentRegistry.ts → components/ComponentRegistry.ts} +148 -54
- package/core/components/Decorators.ts +88 -0
- package/core/components/Interfaces.ts +7 -0
- package/core/components/index.ts +5 -0
- package/core/decorators/EntityHooks.ts +0 -3
- package/core/decorators/IndexedField.ts +26 -0
- package/core/decorators/ScheduledTask.ts +0 -47
- package/core/events/EntityLifecycleEvents.ts +1 -1
- package/core/health.ts +112 -0
- package/core/metadata/definitions/ArcheType.ts +14 -0
- package/core/metadata/definitions/Component.ts +9 -0
- package/core/metadata/definitions/gqlObject.ts +1 -1
- package/core/metadata/index.ts +42 -1
- package/core/metadata/metadata-storage.ts +28 -2
- package/core/middleware/AccessLog.ts +59 -0
- package/core/middleware/RequestId.ts +38 -0
- package/core/middleware/SecurityHeaders.ts +62 -0
- package/core/middleware/index.ts +3 -0
- package/core/scheduler/DistributedLock.ts +266 -0
- package/core/scheduler/index.ts +15 -0
- package/core/validateEnv.ts +92 -0
- package/database/DatabaseHelper.ts +416 -40
- package/database/IndexingStrategy.ts +342 -0
- package/database/PreparedStatementCache.ts +226 -0
- package/database/index.ts +32 -7
- package/database/sqlHelpers.ts +14 -2
- package/endpoints/archetypes.ts +362 -0
- package/endpoints/components.ts +58 -0
- package/endpoints/entity.ts +80 -0
- package/endpoints/index.ts +27 -0
- package/endpoints/query.ts +93 -0
- package/endpoints/stats.ts +76 -0
- package/endpoints/tables.ts +212 -0
- package/endpoints/types.ts +155 -0
- package/gql/ArchetypeOperations.ts +32 -86
- package/gql/Generator.ts +27 -315
- package/gql/GeneratorV2.ts +37 -0
- package/gql/builders/InputTypeBuilder.ts +99 -0
- package/gql/builders/ResolverBuilder.ts +234 -0
- package/gql/builders/TypeDefBuilder.ts +105 -0
- package/gql/builders/index.ts +3 -0
- package/gql/decorators/Upload.ts +1 -1
- package/gql/depthLimit.ts +85 -0
- package/gql/graph/GraphNode.ts +224 -0
- package/gql/graph/SchemaGraph.ts +278 -0
- package/gql/helpers.ts +8 -2
- package/gql/index.ts +56 -4
- package/gql/middleware.ts +79 -0
- package/gql/orchestration/GraphQLSchemaOrchestrator.ts +241 -0
- package/gql/orchestration/index.ts +1 -0
- package/gql/scanner/ServiceScanner.ts +347 -0
- package/gql/schema/index.ts +458 -0
- package/gql/strategies/TypeGenerationStrategy.ts +329 -0
- package/gql/types.ts +1 -0
- package/gql/utils/TypeSignature.ts +220 -0
- package/gql/utils/index.ts +1 -0
- package/gql/visitors/ArchetypePreprocessorVisitor.ts +80 -0
- package/gql/visitors/DeduplicationVisitor.ts +82 -0
- package/gql/visitors/GraphVisitor.ts +78 -0
- package/gql/visitors/ResolverGeneratorVisitor.ts +122 -0
- package/gql/visitors/SchemaGeneratorVisitor.ts +851 -0
- package/gql/visitors/TypeCollectorVisitor.ts +79 -0
- package/gql/visitors/VisitorComposer.ts +96 -0
- package/gql/visitors/index.ts +7 -0
- package/package.json +59 -37
- package/plugins/index.ts +2 -2
- package/query/CTENode.ts +97 -0
- package/query/ComponentInclusionNode.ts +689 -0
- package/query/FilterBuilder.ts +127 -0
- package/query/FilterBuilderRegistry.ts +202 -0
- package/query/OrNode.ts +517 -0
- package/query/OrQuery.ts +42 -0
- package/query/Query.ts +1022 -0
- package/query/QueryContext.ts +170 -0
- package/query/QueryDAG.ts +122 -0
- package/query/QueryNode.ts +65 -0
- package/query/SourceNode.ts +53 -0
- package/query/builders/FullTextSearchBuilder.ts +236 -0
- package/query/index.ts +21 -0
- package/scheduler/index.ts +40 -8
- package/service/Service.ts +2 -1
- package/service/ServiceRegistry.ts +6 -5
- package/{core/storage → storage}/LocalStorageProvider.ts +2 -2
- package/storage/S3StorageProvider.ts +316 -0
- package/{core/storage → storage}/StorageProvider.ts +7 -3
- package/studio/bun.lock +482 -0
- package/studio/index.html +13 -0
- package/studio/package.json +39 -0
- package/studio/postcss.config.js +6 -0
- package/studio/src/components/DataTable.tsx +211 -0
- package/studio/src/components/Layout.tsx +13 -0
- package/studio/src/components/PageContainer.tsx +9 -0
- package/studio/src/components/PageHeader.tsx +13 -0
- package/studio/src/components/SearchBar.tsx +57 -0
- package/studio/src/components/Sidebar.tsx +294 -0
- package/studio/src/components/ui/button.tsx +56 -0
- package/studio/src/components/ui/checkbox.tsx +26 -0
- package/studio/src/components/ui/input.tsx +25 -0
- package/studio/src/hooks/useDataTable.ts +131 -0
- package/studio/src/index.css +36 -0
- package/studio/src/lib/api.ts +186 -0
- package/studio/src/lib/utils.ts +13 -0
- package/studio/src/main.tsx +17 -0
- package/studio/src/pages/ArcheType.tsx +239 -0
- package/studio/src/pages/Components.tsx +124 -0
- package/studio/src/pages/EntityInspector.tsx +302 -0
- package/studio/src/pages/QueryRunner.tsx +246 -0
- package/studio/src/pages/Table.tsx +94 -0
- package/studio/src/pages/Welcome.tsx +241 -0
- package/studio/src/routes.tsx +45 -0
- package/studio/src/store/archeTypeSettings.ts +30 -0
- package/studio/src/store/studio.ts +65 -0
- package/studio/src/utils/columnHelpers.tsx +114 -0
- package/studio/studio-instructions.md +81 -0
- package/studio/tailwind.config.js +77 -0
- package/studio/tsconfig.json +24 -0
- package/studio/utils.ts +54 -0
- package/studio/vite.config.js +19 -0
- package/swagger/generator.ts +1 -1
- package/tests/e2e/http.test.ts +126 -0
- package/tests/fixtures/archetypes/TestUserArchetype.ts +21 -0
- package/tests/fixtures/components/TestOrder.ts +23 -0
- package/tests/fixtures/components/TestProduct.ts +23 -0
- package/tests/fixtures/components/TestUser.ts +20 -0
- package/tests/fixtures/components/index.ts +6 -0
- package/tests/graphql/SchemaGeneration.test.ts +90 -0
- package/tests/graphql/builders/ResolverBuilder.test.ts +223 -0
- package/tests/graphql/builders/TypeDefBuilder.test.ts +153 -0
- package/tests/integration/archetype/ArcheType.persistence.test.ts +241 -0
- package/tests/integration/cache/CacheInvalidation.test.ts +259 -0
- package/tests/integration/entity/Entity.persistence.test.ts +333 -0
- package/tests/integration/query/Query.exec.test.ts +523 -0
- package/tests/pglite-setup.ts +61 -0
- package/tests/setup.ts +164 -0
- package/tests/stress/BenchmarkRunner.ts +203 -0
- package/tests/stress/DataSeeder.ts +190 -0
- package/tests/stress/StressTestReporter.ts +229 -0
- package/tests/stress/cursor-perf-test.ts +171 -0
- package/tests/stress/fixtures/StressTestComponents.ts +58 -0
- package/tests/stress/index.ts +7 -0
- package/tests/stress/scenarios/query-benchmarks.test.ts +285 -0
- package/tests/unit/BatchLoader.test.ts +82 -0
- package/tests/unit/archetype/ArcheType.test.ts +107 -0
- package/tests/unit/cache/CacheManager.test.ts +347 -0
- package/tests/unit/cache/MemoryCache.test.ts +260 -0
- package/tests/unit/cache/RedisCache.test.ts +411 -0
- package/tests/unit/entity/Entity.components.test.ts +244 -0
- package/tests/unit/entity/Entity.test.ts +345 -0
- package/tests/unit/gql/depthLimit.test.ts +203 -0
- package/tests/unit/gql/operationMiddleware.test.ts +293 -0
- package/tests/unit/health/Health.test.ts +129 -0
- package/tests/unit/middleware/AccessLog.test.ts +37 -0
- package/tests/unit/middleware/Middleware.test.ts +98 -0
- package/tests/unit/middleware/RequestId.test.ts +54 -0
- package/tests/unit/middleware/SecurityHeaders.test.ts +66 -0
- package/tests/unit/query/FilterBuilder.test.ts +111 -0
- package/tests/unit/query/Query.test.ts +308 -0
- package/tests/unit/scheduler/DistributedLock.test.ts +274 -0
- package/tests/unit/schema/schema-integration.test.ts +426 -0
- package/tests/unit/schema/schema.test.ts +580 -0
- package/tests/unit/storage/S3StorageProvider.test.ts +571 -0
- package/tests/unit/upload/RestUpload.test.ts +267 -0
- package/tests/unit/validateEnv.test.ts +82 -0
- package/tests/utils/entity-tracker.ts +57 -0
- package/tests/utils/index.ts +13 -0
- package/tests/utils/test-context.ts +149 -0
- package/tsconfig.json +5 -1
- package/types/archetype.types.ts +6 -0
- package/types/hooks.types.ts +1 -1
- package/types/query.types.ts +110 -0
- package/types/scheduler.types.ts +68 -7
- package/types/upload.types.ts +1 -0
- package/{core → upload}/FileValidator.ts +10 -1
- package/upload/RestUpload.ts +130 -0
- package/{core/components → upload}/UploadComponent.ts +11 -11
- package/{core → upload}/UploadManager.ts +3 -3
- package/upload/index.ts +23 -7
- package/utils/UploadHelper.ts +27 -6
- package/utils/cronParser.ts +16 -6
- package/.github/workflows/deploy-docs.yml +0 -57
- package/core/Components.ts +0 -202
- package/core/EntityCache.ts +0 -15
- package/core/Query.ts +0 -880
- package/docs/README.md +0 -149
- package/docs/_coverpage.md +0 -36
- package/docs/_sidebar.md +0 -23
- package/docs/api/core.md +0 -568
- package/docs/api/hooks.md +0 -554
- package/docs/api/index.md +0 -222
- package/docs/api/query.md +0 -678
- package/docs/api/service.md +0 -744
- package/docs/core-concepts/archetypes.md +0 -512
- package/docs/core-concepts/components.md +0 -498
- package/docs/core-concepts/entity.md +0 -314
- package/docs/core-concepts/hooks.md +0 -683
- package/docs/core-concepts/query.md +0 -588
- package/docs/core-concepts/services.md +0 -647
- package/docs/examples/code-examples.md +0 -425
- package/docs/getting-started.md +0 -337
- package/docs/index.html +0 -97
- package/tests/bench/insert.bench.ts +0 -60
- package/tests/bench/relations.bench.ts +0 -270
- package/tests/bench/sorting.bench.ts +0 -416
- package/tests/component-hooks-simple.test.ts +0 -117
- package/tests/component-hooks.test.ts +0 -1461
- package/tests/component.test.ts +0 -339
- package/tests/errorHandling.test.ts +0 -155
- package/tests/hooks.test.ts +0 -667
- package/tests/query-sorting.test.ts +0 -101
- package/tests/query.test.ts +0 -81
- package/tests/relations.test.ts +0 -170
- package/tests/scheduler.test.ts +0 -724
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import { getSerializedMetadataStorage } from "../core/metadata";
|
|
2
|
+
import { findIndicatorComponentName } from "../studio/utils";
|
|
3
|
+
import db from "../database";
|
|
4
|
+
import type {
|
|
5
|
+
StudioArcheTypeQueryParams,
|
|
6
|
+
StudioArcheTypeResponse,
|
|
7
|
+
DeleteArcheTypeEntitiesRequest,
|
|
8
|
+
DeleteResponse,
|
|
9
|
+
ArcheTypeField,
|
|
10
|
+
ArcheTypeEntityRecord,
|
|
11
|
+
} from "./types";
|
|
12
|
+
|
|
13
|
+
export async function handleStudioArcheTypeRecordsRequest(
|
|
14
|
+
archeTypeName: string,
|
|
15
|
+
params: StudioArcheTypeQueryParams = {}
|
|
16
|
+
): Promise<Response> {
|
|
17
|
+
const limit = Math.min(Math.max(params.limit ?? 50, 1), 1000);
|
|
18
|
+
const offset = Math.max(params.offset ?? 0, 0);
|
|
19
|
+
const searchTerm = params.search ?? "";
|
|
20
|
+
const includeDeleted = params.include_deleted ?? false;
|
|
21
|
+
|
|
22
|
+
// Conditional filter: include or exclude soft-deleted rows
|
|
23
|
+
const deletedFilter = includeDeleted ? "" : "AND c.deleted_at IS NULL";
|
|
24
|
+
const deletedFilterBare = includeDeleted ? "" : "AND deleted_at IS NULL";
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const metadataStorage = getSerializedMetadataStorage();
|
|
28
|
+
const archeTypeFields: ArcheTypeField[] | undefined =
|
|
29
|
+
metadataStorage.archeTypes[archeTypeName];
|
|
30
|
+
|
|
31
|
+
if (!archeTypeFields || archeTypeFields.length === 0) {
|
|
32
|
+
return new Response(
|
|
33
|
+
JSON.stringify({
|
|
34
|
+
error: `ArcheType '${archeTypeName}' not found`,
|
|
35
|
+
}),
|
|
36
|
+
{
|
|
37
|
+
status: 404,
|
|
38
|
+
headers: { "Content-Type": "application/json" },
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const indicatorComponentName = findIndicatorComponentName(
|
|
44
|
+
archeTypeName,
|
|
45
|
+
archeTypeFields
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
if (!indicatorComponentName) {
|
|
49
|
+
return new Response(
|
|
50
|
+
JSON.stringify({
|
|
51
|
+
error: `No indicator component found for '${archeTypeName}'`,
|
|
52
|
+
}),
|
|
53
|
+
{
|
|
54
|
+
status: 400,
|
|
55
|
+
headers: { "Content-Type": "application/json" },
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const requiredComponentNames = archeTypeFields
|
|
61
|
+
.filter((field) => !field?.nullable)
|
|
62
|
+
.map((field) => field.componentName);
|
|
63
|
+
|
|
64
|
+
const allComponentNames = archeTypeFields.map(
|
|
65
|
+
(field) => field.componentName
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const requiredComponentCount = requiredComponentNames.length;
|
|
69
|
+
|
|
70
|
+
let entityIdsResult: { entity_id: string }[];
|
|
71
|
+
let totalResult: { count: number }[];
|
|
72
|
+
|
|
73
|
+
const batchSize = limit * 3;
|
|
74
|
+
let currentOffset = offset;
|
|
75
|
+
const validEntities: ArcheTypeEntityRecord[] = [];
|
|
76
|
+
let hasMoreData = true;
|
|
77
|
+
|
|
78
|
+
while (validEntities.length < limit && hasMoreData) {
|
|
79
|
+
if (searchTerm) {
|
|
80
|
+
const searchPattern = `%${searchTerm}%`;
|
|
81
|
+
const componentNamePlaceholders = requiredComponentNames
|
|
82
|
+
.map((_, index) => `$${index + 2}`)
|
|
83
|
+
.join(", ");
|
|
84
|
+
|
|
85
|
+
entityIdsResult = await db.unsafe(
|
|
86
|
+
`SELECT entity_id FROM (
|
|
87
|
+
SELECT entity_id, MAX(created_at) as max_created_at
|
|
88
|
+
FROM components
|
|
89
|
+
WHERE TRUE ${deletedFilterBare}
|
|
90
|
+
GROUP BY entity_id
|
|
91
|
+
HAVING COUNT(DISTINCT CASE WHEN name IN (${componentNamePlaceholders}) THEN name END) = $${
|
|
92
|
+
requiredComponentNames.length + 2
|
|
93
|
+
}
|
|
94
|
+
) archetype_entities
|
|
95
|
+
WHERE entity_id IN (
|
|
96
|
+
SELECT DISTINCT entity_id
|
|
97
|
+
FROM components
|
|
98
|
+
WHERE TRUE ${deletedFilterBare}
|
|
99
|
+
AND (
|
|
100
|
+
data::text ILIKE $1
|
|
101
|
+
OR id::text ILIKE $1
|
|
102
|
+
OR entity_id::text ILIKE $1
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
ORDER BY max_created_at DESC
|
|
106
|
+
LIMIT $${requiredComponentNames.length + 3} OFFSET $${
|
|
107
|
+
requiredComponentNames.length + 4
|
|
108
|
+
}`,
|
|
109
|
+
[
|
|
110
|
+
searchPattern,
|
|
111
|
+
...requiredComponentNames,
|
|
112
|
+
requiredComponentCount,
|
|
113
|
+
batchSize,
|
|
114
|
+
currentOffset,
|
|
115
|
+
]
|
|
116
|
+
);
|
|
117
|
+
} else {
|
|
118
|
+
entityIdsResult = await db.unsafe(
|
|
119
|
+
`SELECT entity_id FROM (
|
|
120
|
+
SELECT c.entity_id, MAX(c.created_at) as max_created_at
|
|
121
|
+
FROM components c
|
|
122
|
+
WHERE c.name = $1
|
|
123
|
+
${deletedFilter}
|
|
124
|
+
GROUP BY c.entity_id
|
|
125
|
+
ORDER BY max_created_at DESC
|
|
126
|
+
LIMIT $2 OFFSET $3
|
|
127
|
+
) sub`,
|
|
128
|
+
[indicatorComponentName, batchSize, currentOffset]
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (entityIdsResult.length === 0) {
|
|
133
|
+
hasMoreData = false;
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const entityIds = entityIdsResult.map((row) => row.entity_id);
|
|
138
|
+
|
|
139
|
+
const entityIdPlaceholders = entityIds
|
|
140
|
+
.map((_, index) => `$${index + 1}`)
|
|
141
|
+
.join(", ");
|
|
142
|
+
const componentNameStartIndex = entityIds.length + 1;
|
|
143
|
+
const componentNamePlaceholders = allComponentNames
|
|
144
|
+
.map((_, index) => `$${componentNameStartIndex + index}`)
|
|
145
|
+
.join(", ");
|
|
146
|
+
|
|
147
|
+
const componentsResult = await db.unsafe(
|
|
148
|
+
`SELECT c.entity_id, c.name, c.data
|
|
149
|
+
FROM components c
|
|
150
|
+
WHERE c.entity_id IN (${entityIdPlaceholders})
|
|
151
|
+
AND c.name IN (${componentNamePlaceholders})
|
|
152
|
+
${deletedFilter}`,
|
|
153
|
+
[...entityIds, ...allComponentNames]
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
// When including deleted, also fetch entity-level deleted_at
|
|
157
|
+
let entityDeletedMap = new Map<string, string | null>();
|
|
158
|
+
if (includeDeleted) {
|
|
159
|
+
const entitiesResult = await db.unsafe(
|
|
160
|
+
`SELECT id, deleted_at FROM entities WHERE id IN (${entityIdPlaceholders})`,
|
|
161
|
+
entityIds
|
|
162
|
+
);
|
|
163
|
+
for (const row of entitiesResult) {
|
|
164
|
+
entityDeletedMap.set(
|
|
165
|
+
row.id as string,
|
|
166
|
+
(row.deleted_at as string) ?? null
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const entityComponentsMap = new Map<string, Map<string, unknown>>();
|
|
172
|
+
|
|
173
|
+
for (const row of componentsResult) {
|
|
174
|
+
const entityId = row.entity_id as string;
|
|
175
|
+
const componentName = row.name as string;
|
|
176
|
+
const componentData = row.data as unknown;
|
|
177
|
+
|
|
178
|
+
if (!entityComponentsMap.has(entityId)) {
|
|
179
|
+
entityComponentsMap.set(entityId, new Map());
|
|
180
|
+
}
|
|
181
|
+
entityComponentsMap
|
|
182
|
+
.get(entityId)!
|
|
183
|
+
.set(componentName, componentData);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
for (const entityId of entityIds) {
|
|
187
|
+
const componentsMap = entityComponentsMap.get(entityId);
|
|
188
|
+
|
|
189
|
+
if (!componentsMap) {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const allRequiredComponentsPresent =
|
|
194
|
+
requiredComponentNames.every((name) =>
|
|
195
|
+
componentsMap.has(name)
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
if (allRequiredComponentsPresent) {
|
|
199
|
+
const componentsObject: Record<string, unknown> = {};
|
|
200
|
+
for (const [name, data] of componentsMap) {
|
|
201
|
+
componentsObject[name] = data;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const record: ArcheTypeEntityRecord = {
|
|
205
|
+
entityId,
|
|
206
|
+
components: componentsObject,
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
if (includeDeleted) {
|
|
210
|
+
record.deleted_at = entityDeletedMap.get(entityId) ?? null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
validEntities.push(record);
|
|
214
|
+
|
|
215
|
+
if (validEntities.length >= limit) {
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
currentOffset += batchSize;
|
|
222
|
+
|
|
223
|
+
if (entityIdsResult.length < batchSize) {
|
|
224
|
+
hasMoreData = false;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (searchTerm) {
|
|
229
|
+
const searchPattern = `%${searchTerm}%`;
|
|
230
|
+
const componentNamePlaceholders = requiredComponentNames
|
|
231
|
+
.map((_, index) => `$${index + 2}`)
|
|
232
|
+
.join(", ");
|
|
233
|
+
|
|
234
|
+
totalResult = await db.unsafe(
|
|
235
|
+
`SELECT COUNT(DISTINCT c.entity_id) as count
|
|
236
|
+
FROM components c
|
|
237
|
+
WHERE TRUE ${deletedFilter}
|
|
238
|
+
AND (
|
|
239
|
+
c.data::text ILIKE $1
|
|
240
|
+
OR c.id::text ILIKE $1
|
|
241
|
+
OR c.entity_id::text ILIKE $1
|
|
242
|
+
)
|
|
243
|
+
AND c.entity_id IN (
|
|
244
|
+
SELECT entity_id
|
|
245
|
+
FROM components
|
|
246
|
+
WHERE TRUE ${deletedFilterBare}
|
|
247
|
+
GROUP BY entity_id
|
|
248
|
+
HAVING COUNT(DISTINCT CASE WHEN name IN (${componentNamePlaceholders}) THEN name END) = $${
|
|
249
|
+
requiredComponentNames.length + 2
|
|
250
|
+
}
|
|
251
|
+
)`,
|
|
252
|
+
[
|
|
253
|
+
searchPattern,
|
|
254
|
+
...requiredComponentNames,
|
|
255
|
+
requiredComponentCount,
|
|
256
|
+
]
|
|
257
|
+
);
|
|
258
|
+
} else {
|
|
259
|
+
totalResult = await db.unsafe(
|
|
260
|
+
`SELECT COUNT(DISTINCT c.entity_id) as count
|
|
261
|
+
FROM components c
|
|
262
|
+
WHERE c.name = $1
|
|
263
|
+
${deletedFilter}`,
|
|
264
|
+
[indicatorComponentName]
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const total = Number(totalResult[0]?.count ?? 0);
|
|
269
|
+
|
|
270
|
+
const responseData: StudioArcheTypeResponse = {
|
|
271
|
+
name: archeTypeName,
|
|
272
|
+
fields: archeTypeFields,
|
|
273
|
+
indicatorComponent: indicatorComponentName,
|
|
274
|
+
entities: validEntities,
|
|
275
|
+
total,
|
|
276
|
+
limit,
|
|
277
|
+
offset,
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
return new Response(JSON.stringify(responseData), {
|
|
281
|
+
headers: { "Content-Type": "application/json" },
|
|
282
|
+
});
|
|
283
|
+
} catch (error) {
|
|
284
|
+
const errorMessage =
|
|
285
|
+
error instanceof Error ? error.message : "Unknown error";
|
|
286
|
+
return new Response(
|
|
287
|
+
JSON.stringify({
|
|
288
|
+
error: `Failed to fetch archetype data: ${errorMessage}`,
|
|
289
|
+
}),
|
|
290
|
+
{
|
|
291
|
+
status: 500,
|
|
292
|
+
headers: { "Content-Type": "application/json" },
|
|
293
|
+
}
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export async function handleStudioArcheTypeDeleteRequest(
|
|
299
|
+
archeTypeName: string,
|
|
300
|
+
requestBody: DeleteArcheTypeEntitiesRequest
|
|
301
|
+
): Promise<Response> {
|
|
302
|
+
const { entityIds } = requestBody;
|
|
303
|
+
|
|
304
|
+
if (!entityIds || !Array.isArray(entityIds) || entityIds.length === 0) {
|
|
305
|
+
return new Response(
|
|
306
|
+
JSON.stringify({
|
|
307
|
+
error: "entityIds array is required and must not be empty",
|
|
308
|
+
}),
|
|
309
|
+
{
|
|
310
|
+
status: 400,
|
|
311
|
+
headers: { "Content-Type": "application/json" },
|
|
312
|
+
}
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
const idPlaceholders = entityIds
|
|
318
|
+
.map((_, index) => `$${index + 1}`)
|
|
319
|
+
.join(", ");
|
|
320
|
+
|
|
321
|
+
// Delete in correct order to avoid foreign key constraint violations
|
|
322
|
+
// 1. Delete from entity_components (junction table)
|
|
323
|
+
await db.unsafe(
|
|
324
|
+
`DELETE FROM entity_components WHERE entity_id IN (${idPlaceholders})`,
|
|
325
|
+
entityIds
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
// 2. Delete from components
|
|
329
|
+
await db.unsafe(
|
|
330
|
+
`DELETE FROM components WHERE entity_id IN (${idPlaceholders})`,
|
|
331
|
+
entityIds
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
// 3. Delete from entities
|
|
335
|
+
await db.unsafe(
|
|
336
|
+
`DELETE FROM entities WHERE id IN (${idPlaceholders})`,
|
|
337
|
+
entityIds
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
const responseData: DeleteResponse = {
|
|
341
|
+
success: true,
|
|
342
|
+
deletedCount: entityIds.length,
|
|
343
|
+
message: `Successfully deleted ${entityIds.length} entity(ies) of type ${archeTypeName}`,
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
return new Response(JSON.stringify(responseData), {
|
|
347
|
+
headers: { "Content-Type": "application/json" },
|
|
348
|
+
});
|
|
349
|
+
} catch (error) {
|
|
350
|
+
const errorMessage =
|
|
351
|
+
error instanceof Error ? error.message : "Unknown error";
|
|
352
|
+
return new Response(
|
|
353
|
+
JSON.stringify({
|
|
354
|
+
error: `Failed to delete entities: ${errorMessage}`,
|
|
355
|
+
}),
|
|
356
|
+
{
|
|
357
|
+
status: 500,
|
|
358
|
+
headers: { "Content-Type": "application/json" },
|
|
359
|
+
}
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import db from "../database";
|
|
2
|
+
import { GenerateTableName } from "../database/DatabaseHelper";
|
|
3
|
+
import type { ComponentTypeInfo, StudioComponentsResponse } from "./types";
|
|
4
|
+
|
|
5
|
+
export async function handleStudioComponentsRequest(): Promise<Response> {
|
|
6
|
+
try {
|
|
7
|
+
// Get distinct component names with entity counts
|
|
8
|
+
const componentRows = await db`
|
|
9
|
+
SELECT name, COUNT(DISTINCT entity_id) as entity_count
|
|
10
|
+
FROM components
|
|
11
|
+
WHERE deleted_at IS NULL
|
|
12
|
+
GROUP BY name
|
|
13
|
+
ORDER BY entity_count DESC
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
const components: ComponentTypeInfo[] = [];
|
|
17
|
+
|
|
18
|
+
for (const row of componentRows) {
|
|
19
|
+
const name = row.name as string;
|
|
20
|
+
const entityCount = Number(row.entity_count);
|
|
21
|
+
const partitionTable = GenerateTableName(name);
|
|
22
|
+
|
|
23
|
+
// Get sample row to discover JSONB field shape
|
|
24
|
+
const sampleResult = await db.unsafe(
|
|
25
|
+
`SELECT data FROM components WHERE name = $1 AND deleted_at IS NULL AND data IS NOT NULL LIMIT 1`,
|
|
26
|
+
[name]
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
let fields: string[] = [];
|
|
30
|
+
if (sampleResult.length > 0 && sampleResult[0].data) {
|
|
31
|
+
const sampleData = sampleResult[0].data;
|
|
32
|
+
if (typeof sampleData === "object" && sampleData !== null) {
|
|
33
|
+
fields = Object.keys(sampleData as Record<string, unknown>);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
components.push({ name, entityCount, partitionTable, fields });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const responseData: StudioComponentsResponse = { components };
|
|
41
|
+
|
|
42
|
+
return new Response(JSON.stringify(responseData), {
|
|
43
|
+
headers: { "Content-Type": "application/json" },
|
|
44
|
+
});
|
|
45
|
+
} catch (error) {
|
|
46
|
+
const errorMessage =
|
|
47
|
+
error instanceof Error ? error.message : "Unknown error";
|
|
48
|
+
return new Response(
|
|
49
|
+
JSON.stringify({
|
|
50
|
+
error: `Failed to fetch components: ${errorMessage}`,
|
|
51
|
+
}),
|
|
52
|
+
{
|
|
53
|
+
status: 500,
|
|
54
|
+
headers: { "Content-Type": "application/json" },
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import db from "../database";
|
|
2
|
+
import type { EntityInspectorResponse } from "./types";
|
|
3
|
+
|
|
4
|
+
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
5
|
+
|
|
6
|
+
export async function handleEntityInspectorRequest(
|
|
7
|
+
entityId: string
|
|
8
|
+
): Promise<Response> {
|
|
9
|
+
if (!entityId || !UUID_REGEX.test(entityId)) {
|
|
10
|
+
return new Response(
|
|
11
|
+
JSON.stringify({ error: "Invalid entity ID format. Expected a UUID." }),
|
|
12
|
+
{
|
|
13
|
+
status: 400,
|
|
14
|
+
headers: { "Content-Type": "application/json" },
|
|
15
|
+
}
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const entityResult = await db`
|
|
21
|
+
SELECT id, created_at, updated_at, deleted_at
|
|
22
|
+
FROM entities
|
|
23
|
+
WHERE id = ${entityId}
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
if (entityResult.length === 0) {
|
|
27
|
+
return new Response(
|
|
28
|
+
JSON.stringify({ error: `Entity '${entityId}' not found` }),
|
|
29
|
+
{
|
|
30
|
+
status: 404,
|
|
31
|
+
headers: { "Content-Type": "application/json" },
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const entity = entityResult[0];
|
|
37
|
+
|
|
38
|
+
// Fetch ALL components for this entity (including soft-deleted)
|
|
39
|
+
const componentsResult = await db`
|
|
40
|
+
SELECT id, name, type_id, data, created_at, updated_at, deleted_at
|
|
41
|
+
FROM components
|
|
42
|
+
WHERE entity_id = ${entityId}
|
|
43
|
+
ORDER BY name ASC, created_at ASC
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
const responseData: EntityInspectorResponse = {
|
|
47
|
+
entity: {
|
|
48
|
+
id: entity.id as string,
|
|
49
|
+
created_at: entity.created_at as string,
|
|
50
|
+
updated_at: entity.updated_at as string,
|
|
51
|
+
deleted_at: (entity.deleted_at as string) ?? null,
|
|
52
|
+
},
|
|
53
|
+
components: componentsResult.map((row: Record<string, unknown>) => ({
|
|
54
|
+
id: row.id as string,
|
|
55
|
+
name: row.name as string,
|
|
56
|
+
type_id: row.type_id as string,
|
|
57
|
+
data: row.data as unknown,
|
|
58
|
+
created_at: row.created_at as string,
|
|
59
|
+
updated_at: row.updated_at as string,
|
|
60
|
+
deleted_at: (row.deleted_at as string) ?? null,
|
|
61
|
+
})),
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return new Response(JSON.stringify(responseData), {
|
|
65
|
+
headers: { "Content-Type": "application/json" },
|
|
66
|
+
});
|
|
67
|
+
} catch (error) {
|
|
68
|
+
const errorMessage =
|
|
69
|
+
error instanceof Error ? error.message : "Unknown error";
|
|
70
|
+
return new Response(
|
|
71
|
+
JSON.stringify({
|
|
72
|
+
error: `Failed to fetch entity: ${errorMessage}`,
|
|
73
|
+
}),
|
|
74
|
+
{
|
|
75
|
+
status: 500,
|
|
76
|
+
headers: { "Content-Type": "application/json" },
|
|
77
|
+
}
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {
|
|
2
|
+
handleStudioTableRequest,
|
|
3
|
+
handleStudioTableDeleteRequest,
|
|
4
|
+
handleGetTables,
|
|
5
|
+
} from "./tables";
|
|
6
|
+
import {
|
|
7
|
+
handleStudioArcheTypeRecordsRequest,
|
|
8
|
+
handleStudioArcheTypeDeleteRequest,
|
|
9
|
+
} from "./archetypes";
|
|
10
|
+
import { handleEntityInspectorRequest } from "./entity";
|
|
11
|
+
import { handleStudioStatsRequest } from "./stats";
|
|
12
|
+
import { handleStudioComponentsRequest } from "./components";
|
|
13
|
+
import { handleStudioQueryRequest } from "./query";
|
|
14
|
+
|
|
15
|
+
const studioEndpoint = {
|
|
16
|
+
handleStudioTableRequest,
|
|
17
|
+
handleStudioArcheTypeRecordsRequest,
|
|
18
|
+
handleStudioTableDeleteRequest,
|
|
19
|
+
handleStudioArcheTypeDeleteRequest,
|
|
20
|
+
handleEntityInspectorRequest,
|
|
21
|
+
handleStudioStatsRequest,
|
|
22
|
+
handleStudioComponentsRequest,
|
|
23
|
+
handleStudioQueryRequest,
|
|
24
|
+
getTables: handleGetTables,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export default studioEndpoint;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import db from "../database";
|
|
2
|
+
import type { StudioQueryRequest, StudioQueryResponse } from "./types";
|
|
3
|
+
|
|
4
|
+
const FORBIDDEN_KEYWORDS = /\b(INSERT|UPDATE|DELETE|DROP|ALTER|CREATE|TRUNCATE|GRANT|REVOKE|COPY|EXECUTE|DO)\b/i;
|
|
5
|
+
const MAX_ROWS = 500;
|
|
6
|
+
const QUERY_TIMEOUT_MS = 10_000;
|
|
7
|
+
|
|
8
|
+
export async function handleStudioQueryRequest(
|
|
9
|
+
requestBody: StudioQueryRequest
|
|
10
|
+
): Promise<Response> {
|
|
11
|
+
// Only allow in non-production
|
|
12
|
+
if (process.env.NODE_ENV === "production") {
|
|
13
|
+
return new Response(
|
|
14
|
+
JSON.stringify({ error: "Query runner is disabled in production" }),
|
|
15
|
+
{
|
|
16
|
+
status: 403,
|
|
17
|
+
headers: { "Content-Type": "application/json" },
|
|
18
|
+
}
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const { sql } = requestBody;
|
|
23
|
+
|
|
24
|
+
if (!sql || typeof sql !== "string" || sql.trim().length === 0) {
|
|
25
|
+
return new Response(
|
|
26
|
+
JSON.stringify({ error: "SQL query is required" }),
|
|
27
|
+
{
|
|
28
|
+
status: 400,
|
|
29
|
+
headers: { "Content-Type": "application/json" },
|
|
30
|
+
}
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const trimmed = sql.trim();
|
|
35
|
+
|
|
36
|
+
// Block write operations
|
|
37
|
+
if (FORBIDDEN_KEYWORDS.test(trimmed)) {
|
|
38
|
+
return new Response(
|
|
39
|
+
JSON.stringify({
|
|
40
|
+
error: "Only read-only (SELECT) queries are allowed",
|
|
41
|
+
}),
|
|
42
|
+
{
|
|
43
|
+
status: 400,
|
|
44
|
+
headers: { "Content-Type": "application/json" },
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Enforce LIMIT if not present
|
|
50
|
+
const hasLimit = /\bLIMIT\b/i.test(trimmed);
|
|
51
|
+
const queryToRun = hasLimit ? trimmed : `${trimmed} LIMIT ${MAX_ROWS}`;
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const startTime = Date.now();
|
|
55
|
+
|
|
56
|
+
const result = await Promise.race([
|
|
57
|
+
db.unsafe(queryToRun),
|
|
58
|
+
new Promise<never>((_, reject) =>
|
|
59
|
+
setTimeout(
|
|
60
|
+
() => reject(new Error("Query timed out")),
|
|
61
|
+
QUERY_TIMEOUT_MS
|
|
62
|
+
)
|
|
63
|
+
),
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
const duration = Date.now() - startTime;
|
|
67
|
+
|
|
68
|
+
const rows = Array.isArray(result) ? result : [];
|
|
69
|
+
const columns =
|
|
70
|
+
rows.length > 0 ? Object.keys(rows[0] as Record<string, unknown>) : [];
|
|
71
|
+
|
|
72
|
+
const responseData: StudioQueryResponse = {
|
|
73
|
+
columns,
|
|
74
|
+
rows: rows.slice(0, MAX_ROWS) as Record<string, unknown>[],
|
|
75
|
+
rowCount: rows.length,
|
|
76
|
+
duration,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return new Response(JSON.stringify(responseData), {
|
|
80
|
+
headers: { "Content-Type": "application/json" },
|
|
81
|
+
});
|
|
82
|
+
} catch (error) {
|
|
83
|
+
const errorMessage =
|
|
84
|
+
error instanceof Error ? error.message : "Unknown error";
|
|
85
|
+
return new Response(
|
|
86
|
+
JSON.stringify({ error: errorMessage }),
|
|
87
|
+
{
|
|
88
|
+
status: 400,
|
|
89
|
+
headers: { "Content-Type": "application/json" },
|
|
90
|
+
}
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import db from "../database";
|
|
2
|
+
import { getSerializedMetadataStorage } from "../core/metadata";
|
|
3
|
+
import type { StudioStatsResponse, ComponentTypeStats, ArcheTypeStats } from "./types";
|
|
4
|
+
|
|
5
|
+
export async function handleStudioStatsRequest(): Promise<Response> {
|
|
6
|
+
try {
|
|
7
|
+
// Run entity counts and component type counts in parallel
|
|
8
|
+
const [activeCountResult, deletedCountResult, componentTypesResult] =
|
|
9
|
+
await Promise.all([
|
|
10
|
+
db`SELECT COUNT(*) as count FROM entities WHERE deleted_at IS NULL`,
|
|
11
|
+
db`SELECT COUNT(*) as count FROM entities WHERE deleted_at IS NOT NULL`,
|
|
12
|
+
db`SELECT name, COUNT(*) as count FROM components WHERE deleted_at IS NULL GROUP BY name ORDER BY count DESC`,
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
const activeCount = Number(activeCountResult[0]?.count ?? 0);
|
|
16
|
+
const deletedCount = Number(deletedCountResult[0]?.count ?? 0);
|
|
17
|
+
|
|
18
|
+
const componentTypes: ComponentTypeStats[] = componentTypesResult.map(
|
|
19
|
+
(row: Record<string, unknown>) => ({
|
|
20
|
+
name: row.name as string,
|
|
21
|
+
count: Number(row.count),
|
|
22
|
+
})
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
// Derive archetype stats from metadata + component counts
|
|
26
|
+
const metadata = getSerializedMetadataStorage();
|
|
27
|
+
const componentCountMap = new Map(
|
|
28
|
+
componentTypes.map((ct) => [ct.name, ct.count])
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const archetypes: ArcheTypeStats[] = [];
|
|
32
|
+
for (const [name, fields] of Object.entries(metadata.archeTypes)) {
|
|
33
|
+
const requiredComponents = fields.filter((f) => !f.nullable);
|
|
34
|
+
const indicatorComponent =
|
|
35
|
+
requiredComponents.find((f) =>
|
|
36
|
+
f.componentName.endsWith("Tag")
|
|
37
|
+
) ?? requiredComponents[0];
|
|
38
|
+
|
|
39
|
+
archetypes.push({
|
|
40
|
+
name,
|
|
41
|
+
entityCount: indicatorComponent
|
|
42
|
+
? componentCountMap.get(indicatorComponent.componentName) ?? 0
|
|
43
|
+
: 0,
|
|
44
|
+
componentCount: fields.length,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
archetypes.sort((a, b) => b.entityCount - a.entityCount);
|
|
49
|
+
|
|
50
|
+
const responseData: StudioStatsResponse = {
|
|
51
|
+
entities: {
|
|
52
|
+
active: activeCount,
|
|
53
|
+
deleted: deletedCount,
|
|
54
|
+
total: activeCount + deletedCount,
|
|
55
|
+
},
|
|
56
|
+
componentTypes,
|
|
57
|
+
archetypes,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return new Response(JSON.stringify(responseData), {
|
|
61
|
+
headers: { "Content-Type": "application/json" },
|
|
62
|
+
});
|
|
63
|
+
} catch (error) {
|
|
64
|
+
const errorMessage =
|
|
65
|
+
error instanceof Error ? error.message : "Unknown error";
|
|
66
|
+
return new Response(
|
|
67
|
+
JSON.stringify({
|
|
68
|
+
error: `Failed to fetch stats: ${errorMessage}`,
|
|
69
|
+
}),
|
|
70
|
+
{
|
|
71
|
+
status: 500,
|
|
72
|
+
headers: { "Content-Type": "application/json" },
|
|
73
|
+
}
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|