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,110 @@
|
|
|
1
|
+
import type { BaseComponent, ComponentDataType } from "../core/components";
|
|
2
|
+
import type { Entity } from "../core/Entity";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Type constructor for a component class
|
|
6
|
+
*/
|
|
7
|
+
export type ComponentConstructor<T extends BaseComponent = BaseComponent> = new (...args: any[]) => T;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Extracts the component class name from a constructor
|
|
11
|
+
*/
|
|
12
|
+
export type ComponentName<T extends ComponentConstructor> = T extends (new (...args: any[]) => infer C) & { name: infer N }
|
|
13
|
+
? N
|
|
14
|
+
: string;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Extracts component data types from a tuple of component classes.
|
|
18
|
+
* Maps [PositionCtor, VelocityCtor] -> [PositionData, VelocityData]
|
|
19
|
+
*/
|
|
20
|
+
export type ExtractComponentData<T extends readonly ComponentConstructor[]> = {
|
|
21
|
+
[K in keyof T]: T[K] extends ComponentConstructor<infer C>
|
|
22
|
+
? C extends BaseComponent ? ComponentDataType<C> : never
|
|
23
|
+
: never;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Maps component constructors to a record of { ComponentName: ComponentData }.
|
|
28
|
+
* Enables access like: entity.componentData.Position.x
|
|
29
|
+
*/
|
|
30
|
+
export type ComponentRecord<T extends readonly ComponentConstructor[]> = {
|
|
31
|
+
[K in T[number] as K extends (new (...args: any[]) => any) & { name: infer N extends string }
|
|
32
|
+
? N
|
|
33
|
+
: never]: K extends ComponentConstructor<infer C>
|
|
34
|
+
? C extends BaseComponent ? ComponentDataType<C> : never
|
|
35
|
+
: never;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Union of all component constructor types in a tuple.
|
|
40
|
+
* Useful for constraining getTyped() to only accept components from the query.
|
|
41
|
+
*/
|
|
42
|
+
export type ComponentUnion<T extends readonly ComponentConstructor[]> = T[number];
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Entity with typed component access based on components included in query.
|
|
46
|
+
* Provides both async getTyped() and synchronous componentData access.
|
|
47
|
+
*/
|
|
48
|
+
export type TypedEntity<TComponents extends readonly ComponentConstructor[]> = Entity & {
|
|
49
|
+
/**
|
|
50
|
+
* Type-safe async component getter - only available for components in the query.
|
|
51
|
+
* Unlike regular get(), this returns non-null since query guarantees component exists.
|
|
52
|
+
*/
|
|
53
|
+
getTyped<T extends ComponentUnion<TComponents>>(
|
|
54
|
+
ctor: T
|
|
55
|
+
): Promise<T extends ComponentConstructor<infer C>
|
|
56
|
+
? C extends BaseComponent ? ComponentDataType<C> : never
|
|
57
|
+
: never>;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Synchronous access to already-loaded component data.
|
|
61
|
+
* Available immediately after query execution without additional DB calls.
|
|
62
|
+
*/
|
|
63
|
+
componentData: ComponentRecord<TComponents>;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The component constructors that were included in this query.
|
|
67
|
+
* Useful for runtime reflection.
|
|
68
|
+
*/
|
|
69
|
+
readonly _queriedComponents: TComponents;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Result type for ArcheType queries that provides direct typed access to archetype fields.
|
|
74
|
+
* Maps archetype field names to their component data types.
|
|
75
|
+
*/
|
|
76
|
+
export type ArcheTypeResult<T extends object> = {
|
|
77
|
+
/** The underlying entity */
|
|
78
|
+
entity: Entity;
|
|
79
|
+
/** Entity ID shorthand */
|
|
80
|
+
id: string;
|
|
81
|
+
/** Save changes to the entity */
|
|
82
|
+
save(): Promise<void>;
|
|
83
|
+
} & {
|
|
84
|
+
[K in keyof T as T[K] extends BaseComponent ? K : never]:
|
|
85
|
+
T[K] extends BaseComponent ? ComponentDataType<T[K]> : never;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Options for ArcheType queries
|
|
90
|
+
*/
|
|
91
|
+
export interface ArcheTypeQueryOptions {
|
|
92
|
+
/** Skip cache for this query */
|
|
93
|
+
noCache?: boolean;
|
|
94
|
+
/** Include specific relations */
|
|
95
|
+
populateRelations?: boolean;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Filter operators for typed queries
|
|
100
|
+
*/
|
|
101
|
+
export type TypedFilterOperator = 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'notIn' | 'like' | 'ilike';
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* A typed filter for archetype queries
|
|
105
|
+
*/
|
|
106
|
+
export interface TypedFilter<T, K extends keyof T = keyof T> {
|
|
107
|
+
field: K;
|
|
108
|
+
operator: TypedFilterOperator;
|
|
109
|
+
value: T[K] extends object ? Partial<T[K]> : T[K];
|
|
110
|
+
}
|
package/types/scheduler.types.ts
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* Comprehensive TypeScript types for the BunSane scheduler system
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { QueryFilter } from "../
|
|
6
|
+
import type { QueryFilter } from "../query/Query";
|
|
7
|
+
import type { Query } from "../query/Query";
|
|
7
8
|
import type { ComponentTargetConfig } from "../core/EntityHookManager";
|
|
8
9
|
|
|
9
10
|
export enum ScheduleInterval {
|
|
@@ -34,14 +35,32 @@ export interface ScheduledTaskOptions {
|
|
|
34
35
|
retryDelay?: number;
|
|
35
36
|
/** Whether to continue retrying on failure */
|
|
36
37
|
continueOnError?: boolean;
|
|
37
|
-
/**
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Maximum number of entities to process per execution
|
|
40
|
+
* Note: This is applied after the query executes. For better performance,
|
|
41
|
+
* include .take() in your query function instead.
|
|
42
|
+
*/
|
|
40
43
|
maxEntitiesPerExecution?: number;
|
|
41
44
|
/** Whether to enable task metrics collection */
|
|
42
45
|
enableMetrics?: boolean;
|
|
43
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* Component targeting configuration for fine-grained entity selection
|
|
48
|
+
* @deprecated Use query() function instead for better flexibility and readability
|
|
49
|
+
*/
|
|
44
50
|
componentTarget?: ComponentTargetConfig;
|
|
51
|
+
/**
|
|
52
|
+
* Custom query function for advanced entity selection (preferred approach)
|
|
53
|
+
* @example
|
|
54
|
+
* query: () => {
|
|
55
|
+
* return new Query()
|
|
56
|
+
* .with(SessionComponent)
|
|
57
|
+
* .with(PhoneComponent)
|
|
58
|
+
* .without(AuthenticatedTag);
|
|
59
|
+
* }
|
|
60
|
+
*/
|
|
61
|
+
query?: () => Query<any>;
|
|
62
|
+
/** Cron expression (when interval is CRON) */
|
|
63
|
+
cronExpression?: string;
|
|
45
64
|
}
|
|
46
65
|
|
|
47
66
|
export interface ScheduledTaskInfo {
|
|
@@ -49,7 +68,10 @@ export interface ScheduledTaskInfo {
|
|
|
49
68
|
id: string;
|
|
50
69
|
/** Task name */
|
|
51
70
|
name: string;
|
|
52
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* Target component class (legacy - use options.query instead)
|
|
73
|
+
* @deprecated Use options.query for better flexibility
|
|
74
|
+
*/
|
|
53
75
|
componentTarget?: new (...args: any[]) => any;
|
|
54
76
|
/** Schedule interval */
|
|
55
77
|
interval: ScheduleInterval;
|
|
@@ -100,6 +122,12 @@ export interface SchedulerMetrics {
|
|
|
100
122
|
retriedTasks: number;
|
|
101
123
|
/** Task-specific metrics */
|
|
102
124
|
taskMetrics: Record<string, TaskMetrics>;
|
|
125
|
+
/** Number of task executions skipped due to distributed lock unavailability */
|
|
126
|
+
skippedExecutions: number;
|
|
127
|
+
/** Total lock acquisition attempts */
|
|
128
|
+
lockAttempts: number;
|
|
129
|
+
/** Successful lock acquisitions */
|
|
130
|
+
locksAcquired: number;
|
|
103
131
|
}
|
|
104
132
|
|
|
105
133
|
export interface TaskMetrics {
|
|
@@ -140,7 +168,7 @@ export interface TaskExecutionResult {
|
|
|
140
168
|
|
|
141
169
|
export interface SchedulerEvent {
|
|
142
170
|
/** Event type */
|
|
143
|
-
type: 'task.registered' | 'task.executed' | 'task.failed' | 'task.timeout' | 'task.retry' | 'scheduler.started' | 'scheduler.stopped';
|
|
171
|
+
type: 'task.registered' | 'task.executed' | 'task.failed' | 'task.timeout' | 'task.retry' | 'task.skipped' | 'task.lock.acquired' | 'task.lock.released' | 'task.lock.failed' | 'scheduler.started' | 'scheduler.stopped';
|
|
144
172
|
/** Task ID if applicable */
|
|
145
173
|
taskId?: string;
|
|
146
174
|
/** Event timestamp */
|
|
@@ -162,4 +190,37 @@ export interface SchedulerConfig {
|
|
|
162
190
|
enableLogging: boolean;
|
|
163
191
|
/** Whether to run tasks on startup */
|
|
164
192
|
runOnStart: boolean;
|
|
193
|
+
/**
|
|
194
|
+
* Enable distributed locking using PostgreSQL advisory locks.
|
|
195
|
+
* When enabled, only one instance can execute a task at a time.
|
|
196
|
+
* This is essential for multi-instance deployments.
|
|
197
|
+
* @default true
|
|
198
|
+
*/
|
|
199
|
+
distributedLocking?: boolean;
|
|
200
|
+
/**
|
|
201
|
+
* Lock acquisition timeout in milliseconds.
|
|
202
|
+
* If > 0, will retry acquiring the lock until timeout.
|
|
203
|
+
* If 0, immediately skips if lock is not available.
|
|
204
|
+
* @default 0
|
|
205
|
+
*/
|
|
206
|
+
lockTimeout?: number;
|
|
207
|
+
/**
|
|
208
|
+
* Retry interval when attempting to acquire locks (ms).
|
|
209
|
+
* Only used when lockTimeout > 0.
|
|
210
|
+
* @default 100
|
|
211
|
+
*/
|
|
212
|
+
lockRetryInterval?: number;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export interface DistributedLockMetrics {
|
|
216
|
+
/** Total lock acquisition attempts */
|
|
217
|
+
lockAttempts: number;
|
|
218
|
+
/** Successful lock acquisitions */
|
|
219
|
+
locksAcquired: number;
|
|
220
|
+
/** Failed lock acquisitions (another instance holds lock) */
|
|
221
|
+
locksFailed: number;
|
|
222
|
+
/** Lock acquisition timeouts */
|
|
223
|
+
lockTimeouts: number;
|
|
224
|
+
/** Tasks skipped due to lock unavailability */
|
|
225
|
+
tasksSkipped: number;
|
|
165
226
|
}
|
package/types/upload.types.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { UploadConfiguration, ValidationResult } from "../types/upload.types";
|
|
2
|
-
import { logger as MainLogger } from "
|
|
2
|
+
import { logger as MainLogger } from "../core/Logger";
|
|
3
3
|
|
|
4
4
|
const logger = MainLogger.child({ scope: "FileValidator" });
|
|
5
5
|
|
|
@@ -64,6 +64,15 @@ export class FileValidator {
|
|
|
64
64
|
errors.push(...nameValidation.errors);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
// Dangerous content detection
|
|
68
|
+
try {
|
|
69
|
+
if (await this.isDangerous(file)) {
|
|
70
|
+
errors.push("File contains potentially dangerous content");
|
|
71
|
+
}
|
|
72
|
+
} catch (error) {
|
|
73
|
+
warnings.push(`Could not check file safety: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
67
76
|
// Custom validation
|
|
68
77
|
if (config.validation?.customValidators) {
|
|
69
78
|
for (const validator of config.validation.customValidators) {
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { UploadManager } from "./UploadManager";
|
|
2
|
+
import type {
|
|
3
|
+
UploadConfiguration,
|
|
4
|
+
UploadResult,
|
|
5
|
+
UploadErrorCode,
|
|
6
|
+
} from "../types/upload.types";
|
|
7
|
+
import { logger as MainLogger } from "../core/Logger";
|
|
8
|
+
|
|
9
|
+
const logger = MainLogger.child({ scope: "RestUpload" });
|
|
10
|
+
|
|
11
|
+
export interface ParsedUpload {
|
|
12
|
+
file: File;
|
|
13
|
+
fieldName: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface RestUploadOptions {
|
|
17
|
+
config?: Partial<UploadConfiguration>;
|
|
18
|
+
storageProvider?: string;
|
|
19
|
+
maxFiles?: number;
|
|
20
|
+
fieldNames?: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface RestUploadResult {
|
|
24
|
+
success: boolean;
|
|
25
|
+
files: UploadResult[];
|
|
26
|
+
fields: Record<string, string>;
|
|
27
|
+
totalFiles: number;
|
|
28
|
+
successCount: number;
|
|
29
|
+
failureCount: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function parseFormData(
|
|
33
|
+
req: Request,
|
|
34
|
+
): Promise<{ files: ParsedUpload[]; fields: Record<string, string> }> {
|
|
35
|
+
const contentType = req.headers.get("content-type") ?? "";
|
|
36
|
+
if (!contentType.includes("multipart/form-data")) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
"Invalid Content-Type: expected multipart/form-data",
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const formData = await req.formData();
|
|
43
|
+
const files: ParsedUpload[] = [];
|
|
44
|
+
const fields: Record<string, string> = {};
|
|
45
|
+
|
|
46
|
+
for (const [key, value] of formData.entries()) {
|
|
47
|
+
if (typeof value !== "string") {
|
|
48
|
+
files.push({ file: value as File, fieldName: key });
|
|
49
|
+
} else {
|
|
50
|
+
fields[key] = value;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { files, fields };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function handleUpload(
|
|
58
|
+
req: Request,
|
|
59
|
+
options: RestUploadOptions = {},
|
|
60
|
+
): Promise<RestUploadResult> {
|
|
61
|
+
const { config, storageProvider, maxFiles, fieldNames } = options;
|
|
62
|
+
|
|
63
|
+
logger.info("Processing REST upload");
|
|
64
|
+
|
|
65
|
+
const { files: parsed, fields } = await parseFormData(req);
|
|
66
|
+
|
|
67
|
+
const filtered = fieldNames
|
|
68
|
+
? parsed.filter((p) => fieldNames.includes(p.fieldName))
|
|
69
|
+
: parsed;
|
|
70
|
+
|
|
71
|
+
if (maxFiles !== undefined && filtered.length > maxFiles) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Too many files: received ${filtered.length}, maximum is ${maxFiles}`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const manager = UploadManager.getInstance();
|
|
78
|
+
const results: UploadResult[] = await Promise.all(
|
|
79
|
+
filtered.map((p) =>
|
|
80
|
+
manager.uploadFile(p.file, config, storageProvider),
|
|
81
|
+
),
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const successCount = results.filter((r) => r.success).length;
|
|
85
|
+
const failureCount = results.length - successCount;
|
|
86
|
+
|
|
87
|
+
logger.info(
|
|
88
|
+
`REST upload complete: ${successCount} succeeded, ${failureCount} failed`,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
success: failureCount === 0,
|
|
93
|
+
files: results,
|
|
94
|
+
fields,
|
|
95
|
+
totalFiles: results.length,
|
|
96
|
+
successCount,
|
|
97
|
+
failureCount,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function uploadResponse(result: RestUploadResult): Response {
|
|
102
|
+
const status =
|
|
103
|
+
result.failureCount > 0 && result.successCount > 0
|
|
104
|
+
? 207
|
|
105
|
+
: result.failureCount > 0
|
|
106
|
+
? 400
|
|
107
|
+
: 200;
|
|
108
|
+
|
|
109
|
+
return new Response(JSON.stringify(result), {
|
|
110
|
+
status,
|
|
111
|
+
headers: { "Content-Type": "application/json" },
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function uploadErrorResponse(
|
|
116
|
+
error: unknown,
|
|
117
|
+
code: UploadErrorCode = "UPLOAD_FAILED",
|
|
118
|
+
status: number = 400,
|
|
119
|
+
): Response {
|
|
120
|
+
const message =
|
|
121
|
+
error instanceof Error ? error.message : "Unknown upload error";
|
|
122
|
+
|
|
123
|
+
return new Response(
|
|
124
|
+
JSON.stringify({ error: message, code }),
|
|
125
|
+
{
|
|
126
|
+
status,
|
|
127
|
+
headers: { "Content-Type": "application/json" },
|
|
128
|
+
},
|
|
129
|
+
);
|
|
130
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { BaseComponent, Component, CompData } from "../
|
|
2
|
-
import type {
|
|
1
|
+
import { BaseComponent, Component, CompData } from "../core/components";
|
|
2
|
+
import type { UploadResult } from "../types/upload.types";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* UploadComponent - Stores file upload metadata in entities
|
|
@@ -37,15 +37,15 @@ export class UploadComponent extends BaseComponent {
|
|
|
37
37
|
/**
|
|
38
38
|
* Set upload data from UploadResult
|
|
39
39
|
*/
|
|
40
|
-
public setUploadData(data:
|
|
41
|
-
this.uploadId = data.uploadId;
|
|
42
|
-
this.fileName = data.fileName;
|
|
43
|
-
this.originalFileName = data.originalFileName;
|
|
44
|
-
this.mimeType = data.mimeType;
|
|
45
|
-
this.size = data.size;
|
|
46
|
-
this.path = data.path;
|
|
47
|
-
this.url = data.url;
|
|
48
|
-
this.uploadedAt =
|
|
40
|
+
public setUploadData(data: UploadResult): void {
|
|
41
|
+
this.uploadId = data.uploadId ?? "";
|
|
42
|
+
this.fileName = data.fileName ?? "";
|
|
43
|
+
this.originalFileName = data.originalFileName ?? "";
|
|
44
|
+
this.mimeType = data.mimeType ?? "";
|
|
45
|
+
this.size = data.size ?? 0;
|
|
46
|
+
this.path = data.path ?? "";
|
|
47
|
+
this.url = data.url ?? "";
|
|
48
|
+
this.uploadedAt = new Date().toISOString();
|
|
49
49
|
this.metadata = JSON.stringify(data.metadata || {});
|
|
50
50
|
}
|
|
51
51
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { logger as MainLogger } from "
|
|
1
|
+
import { logger as MainLogger } from "../core/Logger";
|
|
2
2
|
import { uuidv7 } from "../utils/uuid";
|
|
3
|
-
import type { StorageProvider } from "
|
|
4
|
-
import { LocalStorageProvider } from "
|
|
3
|
+
import type { StorageProvider } from "../storage/StorageProvider";
|
|
4
|
+
import { LocalStorageProvider } from "../storage/LocalStorageProvider";
|
|
5
5
|
import type { UploadConfiguration, UploadResult, UploadError, FileMetadata } from "../types/upload.types";
|
|
6
6
|
import { FileValidator } from "./FileValidator";
|
|
7
7
|
|
package/upload/index.ts
CHANGED
|
@@ -4,16 +4,21 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// Core Upload System
|
|
7
|
-
export { UploadManager } from "
|
|
8
|
-
export { FileValidator } from "
|
|
7
|
+
export { UploadManager } from "./UploadManager";
|
|
8
|
+
export { FileValidator } from "./FileValidator";
|
|
9
9
|
|
|
10
10
|
// Storage Providers
|
|
11
|
-
export { StorageProvider } from "../
|
|
12
|
-
export { LocalStorageProvider } from "../
|
|
11
|
+
export { StorageProvider } from "../storage/StorageProvider";
|
|
12
|
+
export { LocalStorageProvider } from "../storage/LocalStorageProvider";
|
|
13
|
+
export { S3StorageProvider } from "../storage/S3StorageProvider";
|
|
14
|
+
export type { S3StorageConfig } from "../storage/S3StorageProvider";
|
|
13
15
|
|
|
14
|
-
//
|
|
15
|
-
export {
|
|
16
|
+
// REST Upload Utilities
|
|
17
|
+
export { handleUpload, parseFormData, uploadResponse, uploadErrorResponse } from "./RestUpload";
|
|
18
|
+
export type { ParsedUpload, RestUploadOptions, RestUploadResult } from "./RestUpload";
|
|
16
19
|
|
|
20
|
+
// Components
|
|
21
|
+
export { UploadComponent, ImageMetadataComponent } from "./UploadComponent";
|
|
17
22
|
|
|
18
23
|
// Utilities
|
|
19
24
|
export { UploadHelper } from "../utils/UploadHelper";
|
|
@@ -56,7 +61,9 @@ export type {
|
|
|
56
61
|
} from "../types/upload.types";
|
|
57
62
|
|
|
58
63
|
// Imports for internal use
|
|
59
|
-
import { UploadManager } from "
|
|
64
|
+
import { UploadManager } from "./UploadManager";
|
|
65
|
+
import { S3StorageProvider } from "../storage/S3StorageProvider";
|
|
66
|
+
import type { S3StorageConfig } from "../storage/S3StorageProvider";
|
|
60
67
|
import type { UploadConfiguration } from "../types/upload.types";
|
|
61
68
|
|
|
62
69
|
/**
|
|
@@ -73,6 +80,15 @@ export async function initializeUploadSystem(config?: Partial<UploadConfiguratio
|
|
|
73
80
|
await uploadManager.getStorageProvider("local").initialize();
|
|
74
81
|
}
|
|
75
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Initialize S3 storage and register it with UploadManager
|
|
85
|
+
*/
|
|
86
|
+
export async function initializeS3Storage(config: S3StorageConfig): Promise<void> {
|
|
87
|
+
const provider = new S3StorageProvider(config);
|
|
88
|
+
await provider.initialize();
|
|
89
|
+
UploadManager.getInstance().registerStorageProvider("s3", provider);
|
|
90
|
+
}
|
|
91
|
+
|
|
76
92
|
/**
|
|
77
93
|
* Quick setup functions for common use cases
|
|
78
94
|
*/
|
package/utils/UploadHelper.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { UploadManager } from "../
|
|
2
|
-
import { UploadComponent, ImageMetadataComponent } from "../
|
|
1
|
+
import { UploadManager } from "../upload";
|
|
2
|
+
import { UploadComponent, ImageMetadataComponent } from "../upload/UploadComponent";
|
|
3
3
|
import { Entity } from "../core/Entity";
|
|
4
4
|
import type { UploadConfiguration, UploadResult, BatchUploadResult } from "../types/upload.types";
|
|
5
5
|
import { logger as MainLogger } from "../core/Logger";
|
|
@@ -30,6 +30,7 @@ export class UploadHelper {
|
|
|
30
30
|
// Create and attach upload component
|
|
31
31
|
const uploadComponent = new UploadComponent();
|
|
32
32
|
uploadComponent.setUploadData({
|
|
33
|
+
success: result.success,
|
|
33
34
|
uploadId: result.uploadId,
|
|
34
35
|
fileName: result.fileName!,
|
|
35
36
|
originalFileName: result.originalFileName!,
|
|
@@ -42,11 +43,12 @@ export class UploadHelper {
|
|
|
42
43
|
});
|
|
43
44
|
|
|
44
45
|
entity.add(UploadComponent, uploadComponent.data());
|
|
45
|
-
|
|
46
|
+
|
|
47
|
+
// TODO: For better modularity, user might want to use plugins instead
|
|
46
48
|
// Add image metadata if it's an image
|
|
47
|
-
if (result.mimeType?.startsWith('image/')) {
|
|
48
|
-
|
|
49
|
-
}
|
|
49
|
+
// if (result.mimeType?.startsWith('image/')) {
|
|
50
|
+
// await this.addImageMetadata(entity, file, result);
|
|
51
|
+
// }
|
|
50
52
|
|
|
51
53
|
logger.info(`Upload component attached to entity ${entity.id}`);
|
|
52
54
|
}
|
|
@@ -82,6 +84,7 @@ export class UploadHelper {
|
|
|
82
84
|
// Attach upload component to entity
|
|
83
85
|
const uploadComponent = new UploadComponent();
|
|
84
86
|
uploadComponent.setUploadData({
|
|
87
|
+
success: result.success,
|
|
85
88
|
uploadId: result.uploadId!,
|
|
86
89
|
fileName: result.fileName!,
|
|
87
90
|
originalFileName: result.originalFileName!,
|
|
@@ -226,6 +229,7 @@ export class UploadHelper {
|
|
|
226
229
|
// Create new upload component for target entity
|
|
227
230
|
const newUpload = new UploadComponent();
|
|
228
231
|
newUpload.setUploadData({
|
|
232
|
+
success: true,
|
|
229
233
|
uploadId: targetEntity.id,
|
|
230
234
|
fileName: sourceUpload.fileName,
|
|
231
235
|
originalFileName: sourceUpload.originalFileName,
|
|
@@ -252,6 +256,7 @@ export class UploadHelper {
|
|
|
252
256
|
// Remove from source and add to target
|
|
253
257
|
const newUpload = new UploadComponent();
|
|
254
258
|
newUpload.setUploadData({
|
|
259
|
+
success: true,
|
|
255
260
|
uploadId: targetEntity.id,
|
|
256
261
|
fileName: sourceUpload.fileName,
|
|
257
262
|
originalFileName: sourceUpload.originalFileName,
|
|
@@ -302,4 +307,20 @@ export class UploadHelper {
|
|
|
302
307
|
logger.warn(`Failed to add image metadata: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
303
308
|
}
|
|
304
309
|
}
|
|
310
|
+
|
|
311
|
+
static async deleteFile(path: string): Promise<boolean> {
|
|
312
|
+
try {
|
|
313
|
+
const provider = this.uploadManager.getStorageProvider();
|
|
314
|
+
const success = await provider.delete(path);
|
|
315
|
+
if (success) {
|
|
316
|
+
logger.info(`Deleted file at path: ${path}`);
|
|
317
|
+
} else {
|
|
318
|
+
logger.warn(`File at path ${path} not found for deletion`);
|
|
319
|
+
}
|
|
320
|
+
return success;
|
|
321
|
+
} catch (error) {
|
|
322
|
+
logger.error(`Failed to delete file at path ${path}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
305
326
|
}
|
package/utils/cronParser.ts
CHANGED
|
@@ -259,12 +259,18 @@ export class CronParser {
|
|
|
259
259
|
static getNextExecution(cronFields: CronFields, fromDate: Date = new Date()): Date | null {
|
|
260
260
|
const date = new Date(fromDate.getTime());
|
|
261
261
|
|
|
262
|
-
//
|
|
263
|
-
|
|
264
|
-
|
|
262
|
+
// For 6-field expressions (with seconds), start from the next second
|
|
263
|
+
// For 5-field expressions, start from the next minute
|
|
264
|
+
const hasSeconds = !!cronFields.second;
|
|
265
|
+
if (hasSeconds) {
|
|
266
|
+
date.setSeconds(date.getSeconds() + 1);
|
|
267
|
+
} else {
|
|
268
|
+
date.setMinutes(date.getMinutes() + 1);
|
|
269
|
+
date.setSeconds(0);
|
|
270
|
+
}
|
|
265
271
|
|
|
266
272
|
// Try up to 1 year in the future
|
|
267
|
-
const maxAttempts = 60 * 24 * 365; //
|
|
273
|
+
const maxAttempts = hasSeconds ? 60 * 60 * 24 * 365 : 60 * 24 * 365; // seconds or minutes
|
|
268
274
|
let attempts = 0;
|
|
269
275
|
|
|
270
276
|
while (attempts < maxAttempts) {
|
|
@@ -272,8 +278,12 @@ export class CronParser {
|
|
|
272
278
|
return new Date(date);
|
|
273
279
|
}
|
|
274
280
|
|
|
275
|
-
// Move to next minute
|
|
276
|
-
|
|
281
|
+
// Move to next second or minute
|
|
282
|
+
if (hasSeconds) {
|
|
283
|
+
date.setSeconds(date.getSeconds() + 1);
|
|
284
|
+
} else {
|
|
285
|
+
date.setMinutes(date.getMinutes() + 1);
|
|
286
|
+
}
|
|
277
287
|
attempts++;
|
|
278
288
|
}
|
|
279
289
|
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
name: Deploy Documentation to GitHub Pages
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [ main ]
|
|
6
|
-
paths:
|
|
7
|
-
- 'docs/**'
|
|
8
|
-
- '.github/workflows/deploy-docs.yml'
|
|
9
|
-
pull_request:
|
|
10
|
-
branches: [ main ]
|
|
11
|
-
paths:
|
|
12
|
-
- 'docs/**'
|
|
13
|
-
- '.github/workflows/deploy-docs.yml'
|
|
14
|
-
|
|
15
|
-
jobs:
|
|
16
|
-
build-and-deploy:
|
|
17
|
-
runs-on: ubuntu-latest
|
|
18
|
-
|
|
19
|
-
steps:
|
|
20
|
-
- name: Checkout
|
|
21
|
-
uses: actions/checkout@v4
|
|
22
|
-
|
|
23
|
-
- name: Setup Node.js
|
|
24
|
-
uses: actions/setup-node@v4
|
|
25
|
-
with:
|
|
26
|
-
node-version: '18'
|
|
27
|
-
|
|
28
|
-
- name: Install dependencies
|
|
29
|
-
run: |
|
|
30
|
-
cd docs
|
|
31
|
-
npm install -g docsify-cli@latest
|
|
32
|
-
|
|
33
|
-
- name: Build documentation
|
|
34
|
-
run: |
|
|
35
|
-
cd docs
|
|
36
|
-
# Generate any dynamic content if needed
|
|
37
|
-
echo "Building documentation..."
|
|
38
|
-
|
|
39
|
-
- name: Deploy to GitHub Pages
|
|
40
|
-
if: github.ref == 'refs/heads/main'
|
|
41
|
-
uses: peaceiris/actions-gh-pages@v3
|
|
42
|
-
with:
|
|
43
|
-
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
44
|
-
publish_dir: ./docs
|
|
45
|
-
|
|
46
|
-
validate-links:
|
|
47
|
-
runs-on: ubuntu-latest
|
|
48
|
-
steps:
|
|
49
|
-
- name: Checkout
|
|
50
|
-
uses: actions/checkout@v4
|
|
51
|
-
|
|
52
|
-
- name: Link Checker
|
|
53
|
-
uses: lycheeverse/lychee-action@v1.8.0
|
|
54
|
-
with:
|
|
55
|
-
args: --config docs/.lychee.toml docs/
|
|
56
|
-
env:
|
|
57
|
-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|