bunsane 0.1.0 → 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/.github/workflows/deploy-docs.yml +57 -0
- package/LICENSE.md +1 -1
- package/README.md +2 -28
- package/TODO.md +8 -1
- package/bun.lock +3 -0
- package/config/upload.config.ts +135 -0
- package/core/App.ts +168 -4
- package/core/ArcheType.ts +122 -0
- package/core/BatchLoader.ts +100 -0
- package/core/ComponentRegistry.ts +4 -3
- package/core/Components.ts +2 -2
- package/core/Decorators.ts +15 -8
- package/core/Entity.ts +193 -14
- package/core/EntityCache.ts +15 -0
- package/core/EntityHookManager.ts +855 -0
- package/core/EntityManager.ts +12 -2
- package/core/ErrorHandler.ts +64 -7
- package/core/FileValidator.ts +284 -0
- package/core/Query.ts +503 -85
- package/core/RequestContext.ts +24 -0
- package/core/RequestLoaders.ts +89 -0
- package/core/SchedulerManager.ts +710 -0
- package/core/UploadManager.ts +261 -0
- package/core/components/UploadComponent.ts +206 -0
- package/core/decorators/EntityHooks.ts +190 -0
- package/core/decorators/ScheduledTask.ts +83 -0
- package/core/events/EntityLifecycleEvents.ts +177 -0
- package/core/processors/ImageProcessor.ts +423 -0
- package/core/storage/LocalStorageProvider.ts +290 -0
- package/core/storage/StorageProvider.ts +112 -0
- package/database/DatabaseHelper.ts +183 -58
- package/database/index.ts +5 -5
- package/database/sqlHelpers.ts +7 -0
- package/docs/README.md +149 -0
- package/docs/_coverpage.md +36 -0
- package/docs/_sidebar.md +23 -0
- package/docs/api/core.md +568 -0
- package/docs/api/hooks.md +554 -0
- package/docs/api/index.md +222 -0
- package/docs/api/query.md +678 -0
- package/docs/api/service.md +744 -0
- package/docs/core-concepts/archetypes.md +512 -0
- package/docs/core-concepts/components.md +498 -0
- package/docs/core-concepts/entity.md +314 -0
- package/docs/core-concepts/hooks.md +683 -0
- package/docs/core-concepts/query.md +588 -0
- package/docs/core-concepts/services.md +647 -0
- package/docs/examples/code-examples.md +425 -0
- package/docs/getting-started.md +337 -0
- package/docs/index.html +97 -0
- package/gql/Generator.ts +58 -35
- package/gql/decorators/Upload.ts +176 -0
- package/gql/helpers.ts +67 -0
- package/gql/index.ts +65 -31
- package/gql/types.ts +1 -1
- package/index.ts +79 -11
- package/package.json +19 -10
- package/rest/Generator.ts +3 -0
- package/rest/index.ts +22 -0
- package/service/Service.ts +1 -1
- package/service/ServiceRegistry.ts +10 -6
- package/service/index.ts +12 -1
- package/tests/bench/insert.bench.ts +59 -0
- package/tests/bench/relations.bench.ts +269 -0
- package/tests/bench/sorting.bench.ts +415 -0
- package/tests/component-hooks.test.ts +1409 -0
- package/tests/component.test.ts +338 -0
- package/tests/errorHandling.test.ts +155 -0
- package/tests/hooks.test.ts +666 -0
- package/tests/query-sorting.test.ts +101 -0
- package/tests/relations.test.ts +169 -0
- package/tests/scheduler.test.ts +724 -0
- package/tsconfig.json +35 -34
- package/types/graphql.types.ts +87 -0
- package/types/hooks.types.ts +141 -0
- package/types/scheduler.types.ts +165 -0
- package/types/upload.types.ts +184 -0
- package/upload/index.ts +140 -0
- package/utils/UploadHelper.ts +305 -0
- package/utils/cronParser.ts +366 -0
- package/utils/errorMessages.ts +151 -0
- package/core/Events.ts +0 -0
|
@@ -2,6 +2,34 @@ import db from "database";
|
|
|
2
2
|
import { logger as MainLogger } from "core/Logger";
|
|
3
3
|
const logger = MainLogger.child({ scope: "DatabaseHelper" });
|
|
4
4
|
|
|
5
|
+
const BUNSANE_RELATION_TYPED_COLUMN = process.env.BUNSANE_RELATION_TYPED_COLUMN === 'true' || false;
|
|
6
|
+
|
|
7
|
+
const validateIdentifier = (str: string, maxLength: number = 64): string => {
|
|
8
|
+
if (!str || typeof str !== 'string' || str.length === 0 || str.length > maxLength) {
|
|
9
|
+
throw new Error(`Invalid identifier: ${str}`);
|
|
10
|
+
}
|
|
11
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(str)) {
|
|
12
|
+
throw new Error(`Invalid identifier format: ${str}`);
|
|
13
|
+
}
|
|
14
|
+
return str;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
|
18
|
+
|
|
19
|
+
const retryWithBackoff = async (fn: () => Promise<void>, maxRetries: number = 3, baseDelay: number = 1000) => {
|
|
20
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
21
|
+
try {
|
|
22
|
+
await fn();
|
|
23
|
+
return;
|
|
24
|
+
} catch (error) {
|
|
25
|
+
if (i === maxRetries - 1) throw error;
|
|
26
|
+
const delay = baseDelay * Math.pow(2, i);
|
|
27
|
+
logger.warn(`Operation failed, retrying in ${delay}ms: ${error}`);
|
|
28
|
+
await sleep(delay);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
5
33
|
export const GetSchema = async () => {
|
|
6
34
|
const dbSchema = await db`SELECT table_name
|
|
7
35
|
FROM information_schema.tables
|
|
@@ -20,60 +48,145 @@ export const HasValidBaseTable = async (): Promise<boolean> => {
|
|
|
20
48
|
|
|
21
49
|
export const PrepareDatabase = async () => {
|
|
22
50
|
logger.trace(`Initializing Database.`);
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
51
|
+
try {
|
|
52
|
+
await SetupDatabaseExtensions();
|
|
53
|
+
} catch (error) {
|
|
54
|
+
logger.error(`Failed to setup database extensions: ${error}`);
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
await CreateEntityTable();
|
|
59
|
+
} catch (error) {
|
|
60
|
+
logger.error(`Failed to create entity table: ${error}`);
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
await CreateComponentTable();
|
|
65
|
+
} catch (error) {
|
|
66
|
+
logger.error(`Failed to create component table: ${error}`);
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
await CreateEntityComponentTable();
|
|
71
|
+
} catch (error) {
|
|
72
|
+
logger.error(`Failed to create entity component table: ${error}`);
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
27
75
|
}
|
|
28
76
|
|
|
77
|
+
export const GetDatabaseDataSize = async () => {
|
|
78
|
+
const result = await db`SELECT
|
|
79
|
+
relname AS table_name,
|
|
80
|
+
pg_size_pretty(pg_total_relation_size(oid)) AS total_size_pretty,
|
|
81
|
+
ROUND(pg_total_relation_size(oid) / (1024.0 * 1024.0), 2) AS total_size_mb
|
|
82
|
+
FROM
|
|
83
|
+
pg_class
|
|
84
|
+
WHERE
|
|
85
|
+
relkind = 'r' -- 'r' for regular table, 'p' for partitioned table
|
|
86
|
+
AND relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'public') -- Or your specific schema
|
|
87
|
+
ORDER BY
|
|
88
|
+
pg_total_relation_size(oid) DESC;`;
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
29
93
|
export const SetupDatabaseExtensions = async () => {
|
|
30
|
-
|
|
31
|
-
// await db`CREATE EXTENSION IF NOT EXISTS "uuid-ossp";`;
|
|
32
|
-
return resolve(true);
|
|
33
|
-
})
|
|
94
|
+
// await db`CREATE EXTENSION IF NOT EXISTS "uuid-ossp";`;
|
|
34
95
|
}
|
|
35
96
|
|
|
36
97
|
export const CreateEntityTable = async () => {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
return resolve(true);
|
|
44
|
-
});
|
|
98
|
+
await db`CREATE TABLE IF NOT EXISTS entities (
|
|
99
|
+
id UUID PRIMARY KEY,
|
|
100
|
+
created_at TIMESTAMP DEFAULT NOW(),
|
|
101
|
+
updated_at TIMESTAMP DEFAULT NOW(),
|
|
102
|
+
deleted_at TIMESTAMP
|
|
103
|
+
);`;
|
|
45
104
|
}
|
|
46
105
|
|
|
47
|
-
export const CreateComponentTable = () => {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
106
|
+
export const CreateComponentTable = async () => {
|
|
107
|
+
await db`CREATE TABLE IF NOT EXISTS components (
|
|
108
|
+
id UUID,
|
|
109
|
+
entity_id UUID REFERENCES entities(id),
|
|
110
|
+
type_id varchar(64) NOT NULL,
|
|
111
|
+
name varchar(128),
|
|
112
|
+
data jsonb,
|
|
113
|
+
created_at TIMESTAMP DEFAULT NOW(),
|
|
114
|
+
updated_at TIMESTAMP DEFAULT NOW(),
|
|
115
|
+
deleted_at TIMESTAMP,
|
|
116
|
+
PRIMARY KEY (id, type_id, entity_id)
|
|
117
|
+
) PARTITION BY LIST (type_id);`;
|
|
118
|
+
await db`CREATE INDEX IF NOT EXISTS idx_components_entity_id ON components (entity_id);`
|
|
119
|
+
await db`CREATE INDEX IF NOT EXISTS idx_components_type_id ON components (type_id);`
|
|
120
|
+
await db`CREATE INDEX IF NOT EXISTS idx_components_data_gin ON components USING GIN (data);`
|
|
121
|
+
|
|
122
|
+
// Phase 2A: Add composite indexes for sorting optimization
|
|
123
|
+
await db`CREATE INDEX IF NOT EXISTS idx_components_entity_type_deleted ON components (entity_id, type_id, deleted_at);`
|
|
124
|
+
await db`CREATE INDEX IF NOT EXISTS idx_components_type_deleted ON components (type_id, deleted_at) WHERE deleted_at IS NULL;`
|
|
125
|
+
await db`CREATE INDEX IF NOT EXISTS idx_components_deleted_entity ON components (deleted_at, entity_id) WHERE deleted_at IS NULL;`
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export const UpdateComponentIndexes = async (table_name: string, indexedProperties: string[]) => {
|
|
129
|
+
try {
|
|
130
|
+
table_name = validateIdentifier(table_name);
|
|
131
|
+
indexedProperties = indexedProperties.map(prop => validateIdentifier(prop));
|
|
132
|
+
logger.trace(`Updating indexes for component table: ${table_name}`);
|
|
133
|
+
const indexes_list = await db.unsafe(`
|
|
134
|
+
SELECT indexname
|
|
135
|
+
FROM pg_indexes
|
|
136
|
+
WHERE tablename = '${table_name}'
|
|
137
|
+
`);
|
|
138
|
+
const existingIndexes = indexes_list.map((row: any) => row.indexname);
|
|
139
|
+
const addedIndexes = new Set<string>();
|
|
140
|
+
|
|
141
|
+
// Check and create indexes for any new indexed properties
|
|
142
|
+
if (indexedProperties && indexedProperties.length > 0) {
|
|
143
|
+
for (const prop of indexedProperties) {
|
|
144
|
+
const indexName = `idx_${table_name}_${prop}_gin`;
|
|
145
|
+
if (!existingIndexes.includes(indexName)) {
|
|
146
|
+
logger.trace(`Creating missing index ${indexName} for property ${prop}`);
|
|
147
|
+
await retryWithBackoff(async () => {
|
|
148
|
+
await db.unsafe(`CREATE INDEX CONCURRENTLY IF NOT EXISTS ${indexName} ON ${table_name} USING GIN ((data->'${prop}'))`);
|
|
149
|
+
});
|
|
150
|
+
addedIndexes.add(indexName);
|
|
151
|
+
} else {
|
|
152
|
+
logger.trace(`Index ${indexName} for property ${prop} already exists`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Remove indexes for properties that are no longer indexed
|
|
158
|
+
for (const index of existingIndexes) {
|
|
159
|
+
const match = index.match(new RegExp(`^idx_${table_name}_(.*)_gin$`));
|
|
160
|
+
if (match) {
|
|
161
|
+
const prop = match[1];
|
|
162
|
+
if (!indexedProperties.includes(prop) && !addedIndexes.has(index)) {
|
|
163
|
+
await retryWithBackoff(async () => {
|
|
164
|
+
await db.unsafe(`DROP INDEX CONCURRENTLY IF EXISTS ${index}`);
|
|
165
|
+
});
|
|
166
|
+
logger.info(`Dropped obsolete index ${index} for property ${prop}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} catch (error) {
|
|
171
|
+
logger.error(`Failed to update component indexes for ${table_name}: ${error}`);
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
64
174
|
}
|
|
65
175
|
|
|
66
176
|
|
|
67
177
|
export const CreateComponentPartitionTable = async (comp_name: string, type_id: string, indexedProperties?: string[]) => {
|
|
68
178
|
try {
|
|
179
|
+
comp_name = validateIdentifier(comp_name);
|
|
180
|
+
// type_id is a value, not identifier, so no validation
|
|
181
|
+
if (indexedProperties) {
|
|
182
|
+
indexedProperties = indexedProperties.map(prop => validateIdentifier(prop));
|
|
183
|
+
}
|
|
69
184
|
logger.trace(`Attempt adding partition table for component: ${comp_name}`);
|
|
70
185
|
const table_name = `components_${comp_name.toLowerCase().replace(/\s+/g, '_')}`;
|
|
71
186
|
logger.trace(`Checking for existing partition table: ${table_name}`);
|
|
72
|
-
const existingPartition = await db`
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
AND table_schema = 'public'
|
|
76
|
-
`;
|
|
187
|
+
const existingPartition = await db.unsafe(`SELECT 1 FROM information_schema.tables
|
|
188
|
+
WHERE table_name = '${table_name}'
|
|
189
|
+
AND table_schema = 'public'`);
|
|
77
190
|
logger.trace(`Existing partition check result: ${existingPartition.length > 0 ? 'found' : 'not found'}`);
|
|
78
191
|
|
|
79
192
|
if (existingPartition.length > 0) {
|
|
@@ -82,47 +195,54 @@ export const CreateComponentPartitionTable = async (comp_name: string, type_id:
|
|
|
82
195
|
}
|
|
83
196
|
logger.trace(`Creating partition table: ${table_name}`);
|
|
84
197
|
|
|
85
|
-
await
|
|
198
|
+
await retryWithBackoff(async () => {
|
|
199
|
+
await db.unsafe(`CREATE TABLE IF NOT EXISTS ${table_name}
|
|
86
200
|
PARTITION OF components
|
|
87
|
-
FOR VALUES IN ('${type_id}')
|
|
88
|
-
|
|
89
|
-
if (indexedProperties && indexedProperties.length > 0) {
|
|
90
|
-
for (const prop of indexedProperties) {
|
|
91
|
-
const indexName = `idx_${table_name}_${prop}`;
|
|
92
|
-
await db.unsafe(`CREATE INDEX IF NOT EXISTS ${indexName} ON ${table_name} USING GIN ((data->'${prop}'))`);
|
|
93
|
-
logger.trace(`Created index ${indexName} for property ${prop}`);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
201
|
+
FOR VALUES IN ('${type_id}')`);
|
|
202
|
+
});
|
|
97
203
|
logger.trace(`Successfully created partition table: ${table_name}`);
|
|
204
|
+
|
|
205
|
+
if (BUNSANE_RELATION_TYPED_COLUMN && indexedProperties?.includes('value')) {
|
|
206
|
+
logger.trace(`Adding typed FK column for ${table_name}`);
|
|
207
|
+
await retryWithBackoff(async () => {
|
|
208
|
+
await db.unsafe(`ALTER TABLE ${table_name} ADD COLUMN IF NOT EXISTS fk_id UUID GENERATED ALWAYS AS ((data->>'value')::UUID) STORED`);
|
|
209
|
+
});
|
|
210
|
+
await retryWithBackoff(async () => {
|
|
211
|
+
await db.unsafe(`CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_${table_name}_fk_id ON ${table_name} (fk_id)`);
|
|
212
|
+
});
|
|
213
|
+
logger.trace(`Added fk_id column and index for ${table_name}`);
|
|
214
|
+
}
|
|
98
215
|
|
|
99
216
|
} catch (error) {
|
|
100
217
|
logger.error(`Failed to create component partition table for ${comp_name}: ${error}`);
|
|
101
|
-
|
|
218
|
+
// Graceful degradation: log error without crashing
|
|
102
219
|
}
|
|
103
220
|
}
|
|
104
221
|
|
|
105
222
|
export const DeleteComponentPartitionTable = async (comp_name: string) => {
|
|
106
223
|
try {
|
|
224
|
+
comp_name = validateIdentifier(comp_name);
|
|
107
225
|
const table_name = `components_${comp_name.toLowerCase().replace(/\s+/g, '_')}`;
|
|
108
226
|
|
|
109
|
-
const existingPartition = await db`
|
|
227
|
+
const existingPartition = await db.unsafe(`
|
|
110
228
|
SELECT 1 FROM information_schema.tables
|
|
111
|
-
WHERE table_name = ${table_name}
|
|
229
|
+
WHERE table_name = '${table_name}'
|
|
112
230
|
AND table_schema = 'public'
|
|
113
|
-
|
|
231
|
+
`);
|
|
114
232
|
|
|
115
233
|
if (existingPartition.length === 0) {
|
|
116
234
|
logger.info(`Partition table ${table_name} does not exist`);
|
|
117
235
|
return;
|
|
118
236
|
}
|
|
119
237
|
|
|
120
|
-
await
|
|
238
|
+
await retryWithBackoff(async () => {
|
|
239
|
+
await db.unsafe(`DROP TABLE IF EXISTS ${table_name}`);
|
|
240
|
+
});
|
|
121
241
|
logger.info(`Successfully deleted partition table: ${table_name}`);
|
|
122
242
|
|
|
123
243
|
} catch (error) {
|
|
124
244
|
logger.error(`Failed to delete component partition table for ${comp_name}: ${error}`);
|
|
125
|
-
|
|
245
|
+
// Graceful degradation: log error without crashing
|
|
126
246
|
}
|
|
127
247
|
}
|
|
128
248
|
|
|
@@ -130,9 +250,14 @@ export const CreateEntityComponentTable = async () => {
|
|
|
130
250
|
await db`CREATE TABLE IF NOT EXISTS entity_components (
|
|
131
251
|
entity_id UUID REFERENCES entities(id),
|
|
132
252
|
type_id VARCHAR(64) NOT NULL,
|
|
253
|
+
deleted_at TIMESTAMP,
|
|
133
254
|
UNIQUE(entity_id, type_id)
|
|
134
255
|
);`;
|
|
135
|
-
await db`CREATE INDEX IF NOT EXISTS idx_entity_components_entity_id ON entity_components (entity_id);`
|
|
136
|
-
await db`CREATE INDEX IF NOT EXISTS idx_entity_components_type_id ON entity_components (type_id);`
|
|
137
|
-
await db`CREATE INDEX IF NOT EXISTS idx_entity_components_type_entity ON entity_components (type_id, entity_id);`
|
|
256
|
+
await db`CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_entity_components_entity_id ON entity_components (entity_id);`
|
|
257
|
+
await db`CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_entity_components_type_id ON entity_components (type_id);`
|
|
258
|
+
await db`CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_entity_components_type_entity ON entity_components (type_id, entity_id);`
|
|
259
|
+
|
|
260
|
+
// Phase 2A: Add composite indexes for sorting optimization
|
|
261
|
+
await db`CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_entity_components_type_entity_deleted ON entity_components (type_id, entity_id, deleted_at);`
|
|
262
|
+
await db`CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_entity_components_deleted_type ON entity_components (deleted_at, type_id) WHERE deleted_at IS NULL;`
|
|
138
263
|
}
|
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: 10, //
|
|
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) {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { sql } from 'bun';
|
|
2
|
+
|
|
3
|
+
export function inList<T>(values: T[], paramIndex: number): { sql: string, params: any[], newParamIndex: number } {
|
|
4
|
+
if (values.length === 0) return { sql: '()', params: [], newParamIndex: paramIndex };
|
|
5
|
+
const placeholders = Array.from({length: values.length}, (_, i) => `$${paramIndex + i}`).join(', ');
|
|
6
|
+
return { sql: `(${placeholders})`, params: values, newParamIndex: paramIndex + values.length };
|
|
7
|
+
}
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# BunSane Framework Documentation
|
|
2
|
+
|
|
3
|
+
Welcome to the official documentation for **BunSane** - a batteries-included TypeScript API framework built on the Bun runtime.
|
|
4
|
+
|
|
5
|
+
## 🎯 What Makes BunSane Special?
|
|
6
|
+
|
|
7
|
+
BunSane revolutionizes API development by combining the power of Entity-Component-System (ECS) architecture with modern TypeScript. Built specifically for Bun's high-performance runtime, it provides a flexible foundation for building scalable applications.
|
|
8
|
+
|
|
9
|
+
### Key Differentiators
|
|
10
|
+
|
|
11
|
+
- **ECS Architecture**: Flexible data modeling without traditional ORM limitations
|
|
12
|
+
- **Type-Safe Everything**: Full TypeScript integration with compile-time guarantees
|
|
13
|
+
- **Bun-Native Performance**: Optimized for Bun's speed and efficiency
|
|
14
|
+
- **Component-Based Design**: Build complex entities from reusable components
|
|
15
|
+
- **Built-in GraphQL Support**: GraphQL integration with Yoga
|
|
16
|
+
|
|
17
|
+
## 🚀 Quick Start
|
|
18
|
+
|
|
19
|
+
Get up and running in minutes:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# Install BunSane
|
|
23
|
+
bun install bunsane
|
|
24
|
+
|
|
25
|
+
# Create your first entity
|
|
26
|
+
import { Entity, Component, CompData, BaseComponent } from 'bunsane';
|
|
27
|
+
|
|
28
|
+
@Component
|
|
29
|
+
class UserProfile extends BaseComponent {
|
|
30
|
+
@CompData()
|
|
31
|
+
name: string = '';
|
|
32
|
+
|
|
33
|
+
@CompData()
|
|
34
|
+
email: string = '';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Create and save an entity
|
|
38
|
+
const user = Entity.Create();
|
|
39
|
+
user.add(UserProfile, { name: 'John Doe', email: 'john@example.com' });
|
|
40
|
+
await user.save();
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## 📚 Core Architecture
|
|
44
|
+
|
|
45
|
+
BunSane is built around four fundamental concepts:
|
|
46
|
+
|
|
47
|
+
### 1. **Entities** - Your Data Objects
|
|
48
|
+
Entities are the core data containers in BunSane. Unlike traditional objects, entities are composed of multiple components, allowing for flexible and dynamic data structures.
|
|
49
|
+
|
|
50
|
+
### 2. **Components** - Data Building Blocks
|
|
51
|
+
Components are pure data structures that define specific aspects of your entities. They're decorated with `@CompData()` and automatically persisted to PostgreSQL.
|
|
52
|
+
|
|
53
|
+
### 3. **ArcheTypes** - Entity Templates
|
|
54
|
+
ArcheTypes provide reusable templates for creating entities with predefined component sets, reducing code duplication and ensuring consistency.
|
|
55
|
+
|
|
56
|
+
### 4. **Services** - Business Logic
|
|
57
|
+
Services contain your business logic and can integrate with GraphQL resolvers for API endpoints.
|
|
58
|
+
|
|
59
|
+
## 🔧 Features
|
|
60
|
+
|
|
61
|
+
- **Lifecycle Hooks**: React to entity creation, updates, and deletion
|
|
62
|
+
- **Query System**: Type-safe querying with filtering
|
|
63
|
+
- **Component Registry**: Automatic component discovery and registration
|
|
64
|
+
- **Entity Caching**: Built-in caching for improved performance
|
|
65
|
+
- **GraphQL Integration**: Basic GraphQL support with Yoga
|
|
66
|
+
- **File Upload Support**: Basic file upload handling
|
|
67
|
+
- **Background Tasks**: Simple scheduled task support
|
|
68
|
+
- **Request Context**: Context management for requests
|
|
69
|
+
|
|
70
|
+
## 📖 Documentation Sections
|
|
71
|
+
|
|
72
|
+
### Getting Started
|
|
73
|
+
- **[Installation Guide](getting-started.md)** - Setup and configuration
|
|
74
|
+
- **[Interactive Demo](examples/interactive-demo.md)** - Live code examples
|
|
75
|
+
- **[Code Examples](examples/code-examples.md)** - Complete runnable examples
|
|
76
|
+
|
|
77
|
+
### Core Concepts
|
|
78
|
+
- **[Entities](core-concepts/entity.md)** - Entity lifecycle and management
|
|
79
|
+
- **[Components](core-concepts/components.md)** - Component definitions and patterns
|
|
80
|
+
- **[ArcheTypes](core-concepts/archetypes.md)** - Entity templates and reuse
|
|
81
|
+
- **[Services](core-concepts/services.md)** - Business logic layer
|
|
82
|
+
- **[Query System](core-concepts/query.md)** - Database querying
|
|
83
|
+
- **[Hooks](core-concepts/hooks.md)** - Lifecycle events and customization
|
|
84
|
+
|
|
85
|
+
### Advanced Features
|
|
86
|
+
- **[File Uploads](advanced/uploads.md)** - File handling and storage
|
|
87
|
+
- **[Background Tasks](advanced/scheduler.md)** - Job scheduling and processing
|
|
88
|
+
- **[Performance](advanced/performance.md)** - Optimization strategies
|
|
89
|
+
- **[Security](advanced/security.md)** - Authentication and authorization
|
|
90
|
+
|
|
91
|
+
### API Reference
|
|
92
|
+
- **[Core API](api/core.md)** - Entity, Component, ArcheType classes
|
|
93
|
+
- **[Query API](api/query.md)** - Database operations and querying
|
|
94
|
+
- **[Service API](api/service.md)** - Business logic services
|
|
95
|
+
- **[Hooks API](api/hooks.md)** - Event system and lifecycle
|
|
96
|
+
- **[Upload API](api/upload.md)** - File management
|
|
97
|
+
- **[Scheduler API](api/scheduler.md)** - Background job management
|
|
98
|
+
- **[Performance Guide](api/performance.md)** - Optimization techniques
|
|
99
|
+
- **[Testing Guide](api/testing.md)** - Testing patterns and best practices
|
|
100
|
+
|
|
101
|
+
## 🌟 Real-World Use Cases
|
|
102
|
+
|
|
103
|
+
BunSane excels in applications requiring:
|
|
104
|
+
|
|
105
|
+
- **Content Management Systems** - Flexible content modeling with components
|
|
106
|
+
- **E-commerce Platforms** - Complex product catalogs with dynamic attributes
|
|
107
|
+
- **Business Applications** - Multi-tenant architectures with customizable entities
|
|
108
|
+
- **Data-Intensive Applications** - Structured data with relationships
|
|
109
|
+
|
|
110
|
+
## 🤝 Community & Support
|
|
111
|
+
|
|
112
|
+
- **GitHub**: [yaaruu/bunsane](https://github.com/yaaruu/bunsane)
|
|
113
|
+
- **Issues**: [Report bugs or request features](https://github.com/yaaruu/bunsane/issues)
|
|
114
|
+
- **Discussions**: [Community discussions](https://github.com/yaaruu/bunsane/discussions)
|
|
115
|
+
|
|
116
|
+
## 📈 Project Status
|
|
117
|
+
|
|
118
|
+
**Current Version**: 0.1.0 (Development)
|
|
119
|
+
**Documentation**: ✅ Complete API Reference
|
|
120
|
+
**Status**: Actively developed
|
|
121
|
+
**License**: MIT
|
|
122
|
+
|
|
123
|
+
### ✅ Implemented Features
|
|
124
|
+
- **Core ECS Architecture**: Entity, Component, ArcheType system
|
|
125
|
+
- **Database Integration**: PostgreSQL with automatic schema management
|
|
126
|
+
- **Query System**: Type-safe database querying
|
|
127
|
+
- **Hook System**: Entity and component lifecycle events
|
|
128
|
+
- **Service Layer**: Business logic organization
|
|
129
|
+
- **Basic GraphQL**: GraphQL integration with static schema
|
|
130
|
+
- **File Upload**: Basic file upload handling
|
|
131
|
+
- **Background Tasks**: Simple task scheduling
|
|
132
|
+
- **Component Registry**: Automatic component discovery
|
|
133
|
+
|
|
134
|
+
### 🚧 In Development
|
|
135
|
+
- **Advanced GraphQL**: Dynamic schema generation from services
|
|
136
|
+
- **Full-Text Search**: Search capabilities across entities
|
|
137
|
+
- **Real-time Features**: WebSocket support for live updates
|
|
138
|
+
- **Advanced Caching**: Distributed caching strategies
|
|
139
|
+
- **OpenAPI Integration**: Automatic API documentation
|
|
140
|
+
|
|
141
|
+
### 📋 Planned Features
|
|
142
|
+
- **Custom Component Tables**: Direct column storage instead of JSONB
|
|
143
|
+
- **Filesystem Integration**: Advanced file management
|
|
144
|
+
- **Field Constraints**: Data validation and constraints
|
|
145
|
+
- **OpenAPI Specification**: Automatic API spec generation
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
*Ready to build something amazing? Let's get started!* 🚀
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# BunSane Framework
|
|
2
|
+
|
|
3
|
+
> A batteries-included TypeScript API framework for Bun
|
|
4
|
+
|
|
5
|
+
[Get Started](getting-started.md)
|
|
6
|
+
[GitHub](https://github.com/yaaruu/bunsane)
|
|
7
|
+
|
|
8
|
+
## 🚀 What is BunSane?
|
|
9
|
+
|
|
10
|
+
BunSane is a modern, high-performance TypeScript framework built on Bun runtime that provides:
|
|
11
|
+
|
|
12
|
+
- **Entity-Component-System (ECS)** architecture for flexible data modeling
|
|
13
|
+
- **Basic GraphQL Integration** with Yoga
|
|
14
|
+
- **PostgreSQL integration** with automatic schema management
|
|
15
|
+
- **Lifecycle hooks** for business logic integration
|
|
16
|
+
- **File upload system** with basic handling
|
|
17
|
+
|
|
18
|
+
## ⚡ Key Features
|
|
19
|
+
|
|
20
|
+
- **Type-Safe**: Full TypeScript support with compile-time guarantees
|
|
21
|
+
- **Performance Optimized**: Built for speed with Bun's native performance
|
|
22
|
+
- **Component-Based**: Flexible entity composition with reusable components
|
|
23
|
+
- **Hook System**: Event-driven architecture for entity lifecycle
|
|
24
|
+
- **Extensible**: Clean architecture for adding custom functionality
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
## 🎯 Perfect For
|
|
28
|
+
|
|
29
|
+
- **API Development**: REST and basic GraphQL API creation
|
|
30
|
+
- **Data-Intensive Applications**: Efficient entity management
|
|
31
|
+
- **Content Management**: Basic file upload and handling
|
|
32
|
+
- **Business Applications**: Structured data with relationships
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
*Built with ❤️ using Bun, TypeScript, and PostgreSQL*
|
package/docs/_sidebar.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
- **Getting Started**
|
|
2
|
+
- [Overview](README.md)
|
|
3
|
+
- [Installation](getting-started.md)
|
|
4
|
+
- [Quick Start](getting-started.md#quick-start)
|
|
5
|
+
- [Configuration](getting-started.md#configuration)
|
|
6
|
+
|
|
7
|
+
- **Core Concepts**
|
|
8
|
+
- [Entity System](core-concepts/entity.md)
|
|
9
|
+
- [Components](core-concepts/components.md)
|
|
10
|
+
- [ArcheTypes](core-concepts/archetypes.md)
|
|
11
|
+
- [Services](core-concepts/services.md)
|
|
12
|
+
- [Query System](core-concepts/query.md)
|
|
13
|
+
- [Lifecycle Hooks](core-concepts/hooks.md)
|
|
14
|
+
|
|
15
|
+
- **API Reference**
|
|
16
|
+
- [Core API](api/core.md)
|
|
17
|
+
- [Query API](api/query.md)
|
|
18
|
+
- [Service API](api/service.md)
|
|
19
|
+
- [Hook API](api/hooks.md)
|
|
20
|
+
- [Upload API](api/upload.md)
|
|
21
|
+
|
|
22
|
+
- **Examples & Tutorials**
|
|
23
|
+
- [Code Examples](examples/code-examples.md)
|