bunsane 0.5.1 → 0.5.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/ArcheType.ts +1 -1
- package/core/app/metricsCollector.ts +2 -1
- package/core/app/studioRouter.ts +14 -0
- package/core/archetype/zodSchemaBuilder.ts +6 -1
- package/core/entity/saveEntity.ts +2 -2
- package/core/metadata/metadata-storage.ts +59 -1
- package/database/DatabaseHelper.ts +50 -15
- package/endpoints/entity.ts +86 -1
- package/endpoints/index.ts +2 -1
- package/endpoints/types.ts +25 -0
- package/package.json +1 -1
- package/studio/dist/assets/index-BFzJDkHx.js +254 -0
- package/studio/dist/assets/index-TmEdOhTL.css +1 -0
- package/studio/dist/index.html +2 -2
- package/studio/dist/assets/index-BMZ67Npg.js +0 -254
- package/studio/dist/assets/index-BpbuYz9g.css +0 -1
package/core/ArcheType.ts
CHANGED
|
@@ -1035,7 +1035,7 @@ export class BaseArcheType {
|
|
|
1035
1035
|
|
|
1036
1036
|
public buildFilterBranches(filter?: FilterSchema<any>): any[] {
|
|
1037
1037
|
if (!filter) return [];
|
|
1038
|
-
const branches = [];
|
|
1038
|
+
const branches: Array<{ component: any; filters: Array<{ field: string; operator: any; value: any }> }> = [];
|
|
1039
1039
|
|
|
1040
1040
|
for (const [fieldName, componentCtor] of Object.entries(this.componentMap)) {
|
|
1041
1041
|
const fieldOption = this.fieldOptions[fieldName];
|
|
@@ -2,11 +2,12 @@ import { logger as MainLogger } from "../Logger";
|
|
|
2
2
|
import { SchedulerManager } from "../SchedulerManager";
|
|
3
3
|
import { preparedStatementCache } from "../../database/PreparedStatementCache";
|
|
4
4
|
import { getDbStats } from "../../database/instrumentedDb";
|
|
5
|
+
import type { CacheManager } from "../cache/CacheManager";
|
|
5
6
|
|
|
6
7
|
const logger = MainLogger.child({ scope: "App" });
|
|
7
8
|
|
|
8
9
|
export async function collectMetrics(app: any) {
|
|
9
|
-
let cacheStats = null;
|
|
10
|
+
let cacheStats: Awaited<ReturnType<CacheManager["getStats"]>> | null = null;
|
|
10
11
|
try {
|
|
11
12
|
const { CacheManager } = await import('../cache/CacheManager');
|
|
12
13
|
cacheStats = await CacheManager.getInstance().getStats();
|
package/core/app/studioRouter.ts
CHANGED
|
@@ -20,6 +20,20 @@ export async function routeStudio(
|
|
|
20
20
|
return await studioEndpoint.handleStudioComponentsRequest();
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
if (url.pathname === "/studio/api/entities") {
|
|
24
|
+
const limit = url.searchParams.get("limit");
|
|
25
|
+
const offset = url.searchParams.get("offset");
|
|
26
|
+
const search = url.searchParams.get("search");
|
|
27
|
+
const includeDeleted = url.searchParams.get("include_deleted");
|
|
28
|
+
|
|
29
|
+
return await studioEndpoint.handleEntityListRequest({
|
|
30
|
+
limit: limit ? parseInt(limit, 10) : undefined,
|
|
31
|
+
offset: offset ? parseInt(offset, 10) : undefined,
|
|
32
|
+
search: search ?? undefined,
|
|
33
|
+
include_deleted: includeDeleted === "true",
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
23
37
|
if (url.pathname === "/studio/api/query" && method === "POST") {
|
|
24
38
|
const body = await req.json();
|
|
25
39
|
return await studioEndpoint.handleStudioQueryRequest(body);
|
|
@@ -521,7 +521,12 @@ export function buildZodObjectSchema(
|
|
|
521
521
|
graphqlSchema: graphqlSchemaString,
|
|
522
522
|
});
|
|
523
523
|
|
|
524
|
-
|
|
524
|
+
// Only cache the canonical full variant in the shared map. Function-less /
|
|
525
|
+
// relation-less variants (e.g. from getInputSchema) must not overwrite it,
|
|
526
|
+
// or weaveAllArchetypes welds SDL missing @ArcheTypeFunction fields → resolver/schema mismatch.
|
|
527
|
+
if (!excludeRelations && !excludeFunctions) {
|
|
528
|
+
allArchetypeZodObjects.set(nameFromStorage, r);
|
|
529
|
+
}
|
|
525
530
|
|
|
526
531
|
return r;
|
|
527
532
|
}
|
|
@@ -226,8 +226,8 @@ export async function doSave(entity: Entity, trx: SQL, signal?: AbortSignal): Pr
|
|
|
226
226
|
}
|
|
227
227
|
|
|
228
228
|
// Batch inserts and updates for better performance
|
|
229
|
-
const componentsToInsert = [];
|
|
230
|
-
const componentsToUpdate = [];
|
|
229
|
+
const componentsToInsert: Array<{ id: string; entity_id: string; name: string; type_id: string; data: Record<string, any> }> = [];
|
|
230
|
+
const componentsToUpdate: Array<{ id: string; entity_id: string; name: string; type_id: string; data: Record<string, any> }> = [];
|
|
231
231
|
|
|
232
232
|
for (const comp of entity.components.values()) {
|
|
233
233
|
const compName = comp.constructor.name;
|
|
@@ -4,9 +4,16 @@ import type {
|
|
|
4
4
|
ComponentPropertyMetadata,
|
|
5
5
|
IndexedFieldMetadata
|
|
6
6
|
} from "./definitions/Component";
|
|
7
|
-
import type { ArcheTypeMetadata, ArcheTypeFieldOptions } from './definitions/ArcheType';
|
|
7
|
+
import type { ArcheTypeMetadata, ArcheTypeFieldOptions, ArcheTypeFunctionMetadata } from './definitions/ArcheType';
|
|
8
8
|
import type { RelationOptions } from '../ArcheType';
|
|
9
9
|
|
|
10
|
+
// Mirror of decorators.archetypeFunctionsSymbol — referenced via the global symbol
|
|
11
|
+
// registry to avoid a circular import between metadata-storage and archetype/decorators.
|
|
12
|
+
const archetypeFunctionsSymbol = Symbol.for("bunsane:archetypeFunctions");
|
|
13
|
+
|
|
14
|
+
type ArcheTypeFunctionOptions = ArcheTypeFunctionMetadata["options"];
|
|
15
|
+
type ArcheTypeFunctionHandler = (entity: any, ...args: any[]) => any;
|
|
16
|
+
|
|
10
17
|
function generateTypeId(name: string): string {
|
|
11
18
|
return createHash('sha256').update(name).digest('hex');
|
|
12
19
|
}
|
|
@@ -87,6 +94,57 @@ export class MetadataStorage {
|
|
|
87
94
|
this.archetypes_relations_map.get(archetype_id)!.push({fieldName, relatedArcheType, relationType, options, type});
|
|
88
95
|
}
|
|
89
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Register a computed (@ArcheTypeFunction-equivalent) field at runtime, with no decorator.
|
|
99
|
+
*
|
|
100
|
+
* Wires all three sites the decorator path touches in one call:
|
|
101
|
+
* - prototype symbol array → instances pick it up via `this.functions`
|
|
102
|
+
* - prototype method → resolver invokes `archetype[propertyKey](entity, ...)`
|
|
103
|
+
* - archetype metadata.functions → weaver emits the field in the SDL
|
|
104
|
+
*
|
|
105
|
+
* The archetype must already be registered (via @ArcheType or runtime registration)
|
|
106
|
+
* so its target class is known; throws otherwise.
|
|
107
|
+
*/
|
|
108
|
+
collectArchetypeFunction(
|
|
109
|
+
name: string,
|
|
110
|
+
propertyKey: string,
|
|
111
|
+
handler: ArcheTypeFunctionHandler,
|
|
112
|
+
options?: ArcheTypeFunctionOptions
|
|
113
|
+
) {
|
|
114
|
+
const metadata = this.archetypes.find(a => a.name === name);
|
|
115
|
+
if (!metadata) {
|
|
116
|
+
throw new Error(`Cannot register function '${propertyKey}': archetype '${name}' is not registered`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const prototype = (metadata.target as any).prototype;
|
|
120
|
+
|
|
121
|
+
// 1. prototype symbol array (consumed by BaseArcheType ctor → this.functions)
|
|
122
|
+
if (!prototype[archetypeFunctionsSymbol]) {
|
|
123
|
+
prototype[archetypeFunctionsSymbol] = [];
|
|
124
|
+
}
|
|
125
|
+
const protoFns: ArcheTypeFunctionMetadata[] = prototype[archetypeFunctionsSymbol];
|
|
126
|
+
const protoIdx = protoFns.findIndex(f => f.propertyKey === propertyKey);
|
|
127
|
+
if (protoIdx !== -1) {
|
|
128
|
+
protoFns[protoIdx] = { propertyKey, options };
|
|
129
|
+
} else {
|
|
130
|
+
protoFns.push({ propertyKey, options });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 2. prototype method (invoked by the field resolver)
|
|
134
|
+
prototype[propertyKey] = handler;
|
|
135
|
+
|
|
136
|
+
// 3. metadata.functions (read by the weaver to build SDL)
|
|
137
|
+
if (!metadata.functions) {
|
|
138
|
+
metadata.functions = [];
|
|
139
|
+
}
|
|
140
|
+
const metaIdx = metadata.functions.findIndex(f => f.propertyKey === propertyKey);
|
|
141
|
+
if (metaIdx !== -1) {
|
|
142
|
+
metadata.functions[metaIdx] = { propertyKey, options };
|
|
143
|
+
} else {
|
|
144
|
+
metadata.functions.push({ propertyKey, options });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
90
148
|
collectArcheTypeMetadata(metadata: ArcheTypeMetadata) {
|
|
91
149
|
// Check if archetype already exists and update it
|
|
92
150
|
const existingIndex = this.archetypes.findIndex(
|
|
@@ -77,6 +77,41 @@ export const PrepareDatabase = async () => {
|
|
|
77
77
|
// `entity_components` is no longer created or written. `components`
|
|
78
78
|
// (UNIQUE(entity_id, type_id)) is the single source of membership truth
|
|
79
79
|
// as of Phase 3 of docs/ENTITY_COMPONENTS_REMOVAL_PLAN.md.
|
|
80
|
+
try {
|
|
81
|
+
await MigrateTimestampsToTimestamptz();
|
|
82
|
+
} catch (error) {
|
|
83
|
+
logger.error(`Failed to migrate timestamp columns to timestamptz: ${error}`);
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Auto-migrate base-table timestamp columns from `timestamp without time zone`
|
|
90
|
+
* to `timestamptz`. Idempotent: only ALTERs columns still typed as bare
|
|
91
|
+
* timestamp, so fresh DBs (created with TIMESTAMPTZ DDL) and already-migrated
|
|
92
|
+
* DBs are no-ops. Existing bare-timestamp values are interpreted as UTC — the
|
|
93
|
+
* framework only ever writes them via NOW()/CURRENT_TIMESTAMP, which assume the
|
|
94
|
+
* DB session timezone; UTC is the correct assumption for any DB run in UTC.
|
|
95
|
+
* `components` is partitioned — PostgreSQL propagates the type change to every
|
|
96
|
+
* partition (a rewrite that briefly locks the table; one-time cost).
|
|
97
|
+
*/
|
|
98
|
+
export const MigrateTimestampsToTimestamptz = async () => {
|
|
99
|
+
const targets: Array<{ table: string; columns: string[] }> = [
|
|
100
|
+
{ table: "entities", columns: ["created_at", "updated_at", "deleted_at"] },
|
|
101
|
+
{ table: "components", columns: ["created_at", "updated_at", "deleted_at"] },
|
|
102
|
+
];
|
|
103
|
+
for (const { table, columns } of targets) {
|
|
104
|
+
for (const col of columns) {
|
|
105
|
+
const rows = await db.unsafe(`
|
|
106
|
+
SELECT data_type FROM information_schema.columns
|
|
107
|
+
WHERE table_schema = 'public' AND table_name = '${table}' AND column_name = '${col}'
|
|
108
|
+
`);
|
|
109
|
+
if (rows.length === 0) continue; // table or column absent
|
|
110
|
+
if ((rows[0] as any).data_type !== "timestamp without time zone") continue; // already timestamptz
|
|
111
|
+
logger.warn(`Migrating ${table}.${col} timestamp → timestamptz (assuming stored values are UTC)...`);
|
|
112
|
+
await db.unsafe(`ALTER TABLE ${table} ALTER COLUMN ${col} TYPE timestamptz USING ${col} AT TIME ZONE 'UTC'`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
80
115
|
}
|
|
81
116
|
|
|
82
117
|
export const GetDatabaseDataSize = async () => {
|
|
@@ -101,9 +136,9 @@ export const SetupDatabaseExtensions = async () => {
|
|
|
101
136
|
export const CreateEntityTable = async () => {
|
|
102
137
|
await db`CREATE TABLE IF NOT EXISTS entities (
|
|
103
138
|
id UUID PRIMARY KEY,
|
|
104
|
-
created_at
|
|
105
|
-
updated_at
|
|
106
|
-
deleted_at
|
|
139
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
140
|
+
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
141
|
+
deleted_at TIMESTAMPTZ
|
|
107
142
|
);`;
|
|
108
143
|
|
|
109
144
|
// Add partial index for soft-delete queries - critical for 1M+ scale
|
|
@@ -148,9 +183,9 @@ export const CreateComponentTable = async () => {
|
|
|
148
183
|
type_id varchar(64) NOT NULL,
|
|
149
184
|
name varchar(128),
|
|
150
185
|
data jsonb,
|
|
151
|
-
created_at
|
|
152
|
-
updated_at
|
|
153
|
-
deleted_at
|
|
186
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
187
|
+
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
188
|
+
deleted_at TIMESTAMPTZ,
|
|
154
189
|
PRIMARY KEY (id, type_id),
|
|
155
190
|
UNIQUE(entity_id, type_id)
|
|
156
191
|
) PARTITION BY LIST (type_id);`;
|
|
@@ -257,9 +292,9 @@ export const CreateHashPartitionedComponentTable = async (partitionCount: number
|
|
|
257
292
|
type_id varchar(64) NOT NULL,
|
|
258
293
|
name varchar(128),
|
|
259
294
|
data jsonb,
|
|
260
|
-
created_at
|
|
261
|
-
updated_at
|
|
262
|
-
deleted_at
|
|
295
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
296
|
+
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
297
|
+
deleted_at TIMESTAMPTZ,
|
|
263
298
|
PRIMARY KEY (id, type_id),
|
|
264
299
|
UNIQUE(entity_id, type_id)
|
|
265
300
|
) PARTITION BY HASH (type_id);`;
|
|
@@ -483,9 +518,9 @@ export const CreateEntityComponentTable = async () => {
|
|
|
483
518
|
entity_id UUID REFERENCES entities(id) ON DELETE CASCADE,
|
|
484
519
|
type_id VARCHAR(64) NOT NULL,
|
|
485
520
|
component_id UUID,
|
|
486
|
-
created_at
|
|
487
|
-
updated_at
|
|
488
|
-
deleted_at
|
|
521
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
522
|
+
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
523
|
+
deleted_at TIMESTAMPTZ,
|
|
489
524
|
UNIQUE(entity_id, type_id)
|
|
490
525
|
);`;
|
|
491
526
|
const concurrently = process.env.USE_PGLITE ? '' : ' CONCURRENTLY';
|
|
@@ -646,9 +681,9 @@ export const BenchmarkPartitionCounts = async (partitionCounts: number[] = [8, 1
|
|
|
646
681
|
type_id varchar(64) NOT NULL,
|
|
647
682
|
name varchar(128),
|
|
648
683
|
data jsonb,
|
|
649
|
-
created_at
|
|
650
|
-
updated_at
|
|
651
|
-
deleted_at
|
|
684
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
685
|
+
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
686
|
+
deleted_at TIMESTAMPTZ,
|
|
652
687
|
PRIMARY KEY (id, type_id),
|
|
653
688
|
UNIQUE(entity_id, type_id)
|
|
654
689
|
) PARTITION BY HASH (type_id);`);
|
package/endpoints/entity.ts
CHANGED
|
@@ -1,8 +1,93 @@
|
|
|
1
1
|
import db from "../database";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
EntityInspectorResponse,
|
|
4
|
+
StudioEntityListQueryParams,
|
|
5
|
+
StudioEntityListResponse,
|
|
6
|
+
EntityListItem,
|
|
7
|
+
} from "./types";
|
|
3
8
|
|
|
4
9
|
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
10
|
|
|
11
|
+
export async function handleEntityListRequest(
|
|
12
|
+
params: StudioEntityListQueryParams = {}
|
|
13
|
+
): Promise<Response> {
|
|
14
|
+
const limit = Math.min(Math.max(params.limit ?? 50, 1), 1000);
|
|
15
|
+
const offset = Math.max(params.offset ?? 0, 0);
|
|
16
|
+
const searchTerm = params.search?.trim() ?? "";
|
|
17
|
+
const includeDeleted = params.include_deleted ?? false;
|
|
18
|
+
|
|
19
|
+
const deletedFilter = includeDeleted ? "" : "AND e.deleted_at IS NULL";
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
let rows: Record<string, unknown>[];
|
|
23
|
+
let totalResult: { count: number }[];
|
|
24
|
+
|
|
25
|
+
if (searchTerm) {
|
|
26
|
+
const searchPattern = `%${searchTerm}%`;
|
|
27
|
+
rows = await db.unsafe(
|
|
28
|
+
`SELECT e.id, e.created_at, e.updated_at, e.deleted_at,
|
|
29
|
+
(SELECT COUNT(*) FROM components c
|
|
30
|
+
WHERE c.entity_id = e.id AND c.deleted_at IS NULL) AS component_count
|
|
31
|
+
FROM entities e
|
|
32
|
+
WHERE e.id::text ILIKE $1 ${deletedFilter}
|
|
33
|
+
ORDER BY e.created_at DESC NULLS LAST
|
|
34
|
+
LIMIT $2 OFFSET $3`,
|
|
35
|
+
[searchPattern, limit, offset]
|
|
36
|
+
);
|
|
37
|
+
totalResult = await db.unsafe(
|
|
38
|
+
`SELECT COUNT(*) AS count FROM entities e
|
|
39
|
+
WHERE e.id::text ILIKE $1 ${deletedFilter}`,
|
|
40
|
+
[searchPattern]
|
|
41
|
+
);
|
|
42
|
+
} else {
|
|
43
|
+
rows = await db.unsafe(
|
|
44
|
+
`SELECT e.id, e.created_at, e.updated_at, e.deleted_at,
|
|
45
|
+
(SELECT COUNT(*) FROM components c
|
|
46
|
+
WHERE c.entity_id = e.id AND c.deleted_at IS NULL) AS component_count
|
|
47
|
+
FROM entities e
|
|
48
|
+
WHERE TRUE ${deletedFilter}
|
|
49
|
+
ORDER BY e.created_at DESC NULLS LAST
|
|
50
|
+
LIMIT $1 OFFSET $2`,
|
|
51
|
+
[limit, offset]
|
|
52
|
+
);
|
|
53
|
+
totalResult = await db.unsafe(
|
|
54
|
+
`SELECT COUNT(*) AS count FROM entities e WHERE TRUE ${deletedFilter}`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const entities: EntityListItem[] = rows.map((row) => ({
|
|
59
|
+
id: row.id as string,
|
|
60
|
+
created_at: row.created_at as string,
|
|
61
|
+
updated_at: row.updated_at as string,
|
|
62
|
+
deleted_at: (row.deleted_at as string) ?? null,
|
|
63
|
+
component_count: Number(row.component_count ?? 0),
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
const responseData: StudioEntityListResponse = {
|
|
67
|
+
entities,
|
|
68
|
+
total: Number(totalResult[0]?.count ?? 0),
|
|
69
|
+
limit,
|
|
70
|
+
offset,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return new Response(JSON.stringify(responseData), {
|
|
74
|
+
headers: { "Content-Type": "application/json" },
|
|
75
|
+
});
|
|
76
|
+
} catch (error) {
|
|
77
|
+
const errorMessage =
|
|
78
|
+
error instanceof Error ? error.message : "Unknown error";
|
|
79
|
+
return new Response(
|
|
80
|
+
JSON.stringify({
|
|
81
|
+
error: `Failed to fetch entities: ${errorMessage}`,
|
|
82
|
+
}),
|
|
83
|
+
{
|
|
84
|
+
status: 500,
|
|
85
|
+
headers: { "Content-Type": "application/json" },
|
|
86
|
+
}
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
6
91
|
export async function handleEntityInspectorRequest(
|
|
7
92
|
entityId: string
|
|
8
93
|
): Promise<Response> {
|
package/endpoints/index.ts
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
handleStudioArcheTypeRecordsRequest,
|
|
8
8
|
handleStudioArcheTypeDeleteRequest,
|
|
9
9
|
} from "./archetypes";
|
|
10
|
-
import { handleEntityInspectorRequest } from "./entity";
|
|
10
|
+
import { handleEntityInspectorRequest, handleEntityListRequest } from "./entity";
|
|
11
11
|
import { handleStudioStatsRequest } from "./stats";
|
|
12
12
|
import { handleStudioComponentsRequest } from "./components";
|
|
13
13
|
import { handleStudioQueryRequest } from "./query";
|
|
@@ -18,6 +18,7 @@ const studioEndpoint = {
|
|
|
18
18
|
handleStudioTableDeleteRequest,
|
|
19
19
|
handleStudioArcheTypeDeleteRequest,
|
|
20
20
|
handleEntityInspectorRequest,
|
|
21
|
+
handleEntityListRequest,
|
|
21
22
|
handleStudioStatsRequest,
|
|
22
23
|
handleStudioComponentsRequest,
|
|
23
24
|
handleStudioQueryRequest,
|
package/endpoints/types.ts
CHANGED
|
@@ -88,6 +88,28 @@ interface EntityInspectorResponse {
|
|
|
88
88
|
components: EntityComponent[];
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
interface StudioEntityListQueryParams {
|
|
92
|
+
limit?: number;
|
|
93
|
+
offset?: number;
|
|
94
|
+
search?: string;
|
|
95
|
+
include_deleted?: boolean;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
interface EntityListItem {
|
|
99
|
+
id: string;
|
|
100
|
+
created_at: string;
|
|
101
|
+
updated_at: string;
|
|
102
|
+
deleted_at: string | null;
|
|
103
|
+
component_count: number;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
interface StudioEntityListResponse {
|
|
107
|
+
entities: EntityListItem[];
|
|
108
|
+
total: number;
|
|
109
|
+
limit: number;
|
|
110
|
+
offset: number;
|
|
111
|
+
}
|
|
112
|
+
|
|
91
113
|
interface ComponentTypeStats {
|
|
92
114
|
name: string;
|
|
93
115
|
count: number;
|
|
@@ -145,6 +167,9 @@ export type {
|
|
|
145
167
|
DeleteResponse,
|
|
146
168
|
EntityComponent,
|
|
147
169
|
EntityInspectorResponse,
|
|
170
|
+
StudioEntityListQueryParams,
|
|
171
|
+
EntityListItem,
|
|
172
|
+
StudioEntityListResponse,
|
|
148
173
|
ComponentTypeStats,
|
|
149
174
|
ArcheTypeStats,
|
|
150
175
|
StudioStatsResponse,
|