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.
Files changed (82) hide show
  1. package/.github/workflows/deploy-docs.yml +57 -0
  2. package/LICENSE.md +1 -1
  3. package/README.md +2 -28
  4. package/TODO.md +8 -1
  5. package/bun.lock +3 -0
  6. package/config/upload.config.ts +135 -0
  7. package/core/App.ts +168 -4
  8. package/core/ArcheType.ts +122 -0
  9. package/core/BatchLoader.ts +100 -0
  10. package/core/ComponentRegistry.ts +4 -3
  11. package/core/Components.ts +2 -2
  12. package/core/Decorators.ts +15 -8
  13. package/core/Entity.ts +193 -14
  14. package/core/EntityCache.ts +15 -0
  15. package/core/EntityHookManager.ts +855 -0
  16. package/core/EntityManager.ts +12 -2
  17. package/core/ErrorHandler.ts +64 -7
  18. package/core/FileValidator.ts +284 -0
  19. package/core/Query.ts +503 -85
  20. package/core/RequestContext.ts +24 -0
  21. package/core/RequestLoaders.ts +89 -0
  22. package/core/SchedulerManager.ts +710 -0
  23. package/core/UploadManager.ts +261 -0
  24. package/core/components/UploadComponent.ts +206 -0
  25. package/core/decorators/EntityHooks.ts +190 -0
  26. package/core/decorators/ScheduledTask.ts +83 -0
  27. package/core/events/EntityLifecycleEvents.ts +177 -0
  28. package/core/processors/ImageProcessor.ts +423 -0
  29. package/core/storage/LocalStorageProvider.ts +290 -0
  30. package/core/storage/StorageProvider.ts +112 -0
  31. package/database/DatabaseHelper.ts +183 -58
  32. package/database/index.ts +5 -5
  33. package/database/sqlHelpers.ts +7 -0
  34. package/docs/README.md +149 -0
  35. package/docs/_coverpage.md +36 -0
  36. package/docs/_sidebar.md +23 -0
  37. package/docs/api/core.md +568 -0
  38. package/docs/api/hooks.md +554 -0
  39. package/docs/api/index.md +222 -0
  40. package/docs/api/query.md +678 -0
  41. package/docs/api/service.md +744 -0
  42. package/docs/core-concepts/archetypes.md +512 -0
  43. package/docs/core-concepts/components.md +498 -0
  44. package/docs/core-concepts/entity.md +314 -0
  45. package/docs/core-concepts/hooks.md +683 -0
  46. package/docs/core-concepts/query.md +588 -0
  47. package/docs/core-concepts/services.md +647 -0
  48. package/docs/examples/code-examples.md +425 -0
  49. package/docs/getting-started.md +337 -0
  50. package/docs/index.html +97 -0
  51. package/gql/Generator.ts +58 -35
  52. package/gql/decorators/Upload.ts +176 -0
  53. package/gql/helpers.ts +67 -0
  54. package/gql/index.ts +65 -31
  55. package/gql/types.ts +1 -1
  56. package/index.ts +79 -11
  57. package/package.json +19 -10
  58. package/rest/Generator.ts +3 -0
  59. package/rest/index.ts +22 -0
  60. package/service/Service.ts +1 -1
  61. package/service/ServiceRegistry.ts +10 -6
  62. package/service/index.ts +12 -1
  63. package/tests/bench/insert.bench.ts +59 -0
  64. package/tests/bench/relations.bench.ts +269 -0
  65. package/tests/bench/sorting.bench.ts +415 -0
  66. package/tests/component-hooks.test.ts +1409 -0
  67. package/tests/component.test.ts +338 -0
  68. package/tests/errorHandling.test.ts +155 -0
  69. package/tests/hooks.test.ts +666 -0
  70. package/tests/query-sorting.test.ts +101 -0
  71. package/tests/relations.test.ts +169 -0
  72. package/tests/scheduler.test.ts +724 -0
  73. package/tsconfig.json +35 -34
  74. package/types/graphql.types.ts +87 -0
  75. package/types/hooks.types.ts +141 -0
  76. package/types/scheduler.types.ts +165 -0
  77. package/types/upload.types.ts +184 -0
  78. package/upload/index.ts +140 -0
  79. package/utils/UploadHelper.ts +305 -0
  80. package/utils/cronParser.ts +366 -0
  81. package/utils/errorMessages.ts +151 -0
  82. package/core/Events.ts +0 -0
@@ -0,0 +1,176 @@
1
+ import "reflect-metadata";
2
+ import type { UploadDecoratorConfig } from "../../types/upload.types";
3
+ import { UploadManager } from "../../core/UploadManager";
4
+ import { logger as MainLogger } from "../../core/Logger";
5
+
6
+ const logger = MainLogger.child({ scope: "UploadDecorator" });
7
+
8
+ /**
9
+ * Metadata key for upload configuration
10
+ */
11
+ export const UPLOAD_CONFIG_KEY = Symbol("upload:config");
12
+
13
+ /**
14
+ * @Upload decorator for GraphQL mutation parameters
15
+ * Automatically handles file uploads and stores metadata
16
+ */
17
+ export function Upload(config?: UploadDecoratorConfig) {
18
+ return function (target: any, propertyKey: string, parameterIndex: number) {
19
+ logger.trace(`Registering @Upload decorator for ${target.constructor.name}.${propertyKey} parameter ${parameterIndex}`);
20
+
21
+ const existingMetadata = Reflect.getMetadata(UPLOAD_CONFIG_KEY, target, propertyKey) || {};
22
+
23
+ existingMetadata[parameterIndex] = {
24
+ field: config?.field || propertyKey,
25
+ batch: config?.batch || false,
26
+ required: config?.required || false,
27
+ validationMessage: config?.validationMessage,
28
+ ...config
29
+ };
30
+
31
+ Reflect.defineMetadata(UPLOAD_CONFIG_KEY, existingMetadata, target, propertyKey);
32
+ };
33
+ }
34
+
35
+ /**
36
+ * @UploadField decorator for GraphQL field-level upload configuration
37
+ * Used to configure upload behavior for specific fields
38
+ */
39
+ export function UploadField(config: UploadDecoratorConfig) {
40
+ return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
41
+ logger.trace(`Registering @UploadField decorator for ${target.constructor.name}.${propertyKey}`);
42
+
43
+ const originalMethod = descriptor.value;
44
+
45
+ descriptor.value = async function (...args: any[]) {
46
+ const uploadManager = UploadManager.getInstance();
47
+
48
+ // Check if this method has upload parameters
49
+ const uploadMetadata = Reflect.getMetadata(UPLOAD_CONFIG_KEY, target, propertyKey);
50
+
51
+ if (uploadMetadata) {
52
+ // Process uploads before calling the original method
53
+ for (const [paramIndex, uploadConfig] of Object.entries(uploadMetadata)) {
54
+ const paramIdx = parseInt(paramIndex);
55
+ const file = args[paramIdx];
56
+ const config = uploadConfig as any;
57
+
58
+ if (file && file instanceof File) {
59
+ logger.info(`Processing upload for parameter ${paramIdx} in ${target.constructor.name}.${propertyKey}`);
60
+
61
+ try {
62
+ const result = await uploadManager.uploadFile(file, config);
63
+
64
+ if (!result.success) {
65
+ throw new Error(config.validationMessage || result.error?.message || "Upload failed");
66
+ }
67
+
68
+ // Replace file parameter with upload result
69
+ args[paramIdx] = result;
70
+
71
+ } catch (error) {
72
+ logger.error(`Upload failed for ${target.constructor.name}.${propertyKey}: ${error instanceof Error ? error.message : 'Unknown error'}`);
73
+ throw error;
74
+ }
75
+ } else if (config.required) {
76
+ throw new Error(`Required upload file missing for parameter ${paramIdx}`);
77
+ }
78
+ }
79
+ }
80
+
81
+ return await originalMethod.apply(this, args);
82
+ };
83
+
84
+ return descriptor;
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Helper function to extract upload configuration from method metadata
90
+ */
91
+ export function getUploadConfiguration(target: any, propertyKey: string): Record<number, any> | undefined {
92
+ return Reflect.getMetadata(UPLOAD_CONFIG_KEY, target, propertyKey);
93
+ }
94
+
95
+ /**
96
+ * @BatchUpload decorator for handling multiple file uploads
97
+ */
98
+ export function BatchUpload(config?: UploadDecoratorConfig) {
99
+ return Upload({ ...config, batch: true });
100
+ }
101
+
102
+ /**
103
+ * @RequiredUpload decorator for mandatory file uploads
104
+ */
105
+ export function RequiredUpload(config?: UploadDecoratorConfig) {
106
+ return Upload({ ...config, required: true });
107
+ }
108
+
109
+ /**
110
+ * Higher-order decorator factory for common upload patterns
111
+ */
112
+ export class UploadDecorators {
113
+ /**
114
+ * Image upload decorator with image-specific validation
115
+ */
116
+ static Image(config?: Partial<UploadDecoratorConfig>) {
117
+ return Upload({
118
+ ...config,
119
+ maxFileSize: 5 * 1024 * 1024, // 5MB
120
+ allowedMimeTypes: ["image/jpeg", "image/png", "image/gif", "image/webp"],
121
+ allowedExtensions: [".jpg", ".jpeg", ".png", ".gif", ".webp"],
122
+ generateThumbnails: true
123
+ });
124
+ }
125
+
126
+ /**
127
+ * Avatar upload decorator with strict constraints
128
+ */
129
+ static Avatar(config?: Partial<UploadDecoratorConfig>) {
130
+ return Upload({
131
+ ...config,
132
+ required: true,
133
+ maxFileSize: 2 * 1024 * 1024, // 2MB
134
+ allowedMimeTypes: ["image/jpeg", "image/png", "image/webp"],
135
+ allowedExtensions: [".jpg", ".jpeg", ".png", ".webp"],
136
+ generateThumbnails: true,
137
+ namingStrategy: "uuid"
138
+ });
139
+ }
140
+
141
+ /**
142
+ * Document upload decorator
143
+ */
144
+ static Document(config?: Partial<UploadDecoratorConfig>) {
145
+ return Upload({
146
+ ...config,
147
+ maxFileSize: 25 * 1024 * 1024, // 25MB
148
+ allowedMimeTypes: [
149
+ "application/pdf",
150
+ "text/plain",
151
+ "application/msword",
152
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
153
+ ],
154
+ allowedExtensions: [".pdf", ".txt", ".doc", ".docx"],
155
+ validateFileSignature: true
156
+ });
157
+ }
158
+
159
+ /**
160
+ * Secure upload decorator with strict validation
161
+ */
162
+ static Secure(config?: Partial<UploadDecoratorConfig>) {
163
+ return Upload({
164
+ ...config,
165
+ maxFileSize: 1 * 1024 * 1024, // 1MB
166
+ allowedMimeTypes: ["image/jpeg", "image/png"],
167
+ allowedExtensions: [".jpg", ".jpeg", ".png"],
168
+ validateFileSignature: true,
169
+ sanitizeFileName: true,
170
+ validation: {
171
+ scanForMalware: true,
172
+ strictMimeType: true
173
+ }
174
+ });
175
+ }
176
+ }
package/gql/helpers.ts ADDED
@@ -0,0 +1,67 @@
1
+ import { GraphQLFieldTypes } from './types';
2
+
3
+ export type GraphQLType =
4
+ | GraphQLFieldTypes // e.g., "ID!", "String"
5
+ | string // For custom types like "User", "[User]", "[User]!"
6
+ | `${string}!` // For required custom types
7
+ | `[${string}]` // For list types
8
+ | `[${string}]!`; // For required list types
9
+
10
+ export function isValidGraphQLType(type: string): type is GraphQLType {
11
+ const enumValues = Object.values(GraphQLFieldTypes);
12
+ return enumValues.includes(type as GraphQLFieldTypes) ||
13
+ /^(\w+|\[\w+\])(!)?$/.test(type); // Simple regex for custom types/lists
14
+ }
15
+
16
+ export type TypeFromGraphQL<T extends GraphQLType> =
17
+ T extends GraphQLFieldTypes.ID_REQUIRED | GraphQLFieldTypes.ID_OPTIONAL ? string :
18
+ T extends GraphQLFieldTypes.STRING_REQUIRED ? string :
19
+ T extends GraphQLFieldTypes.STRING_OPTIONAL ? string | null :
20
+ T extends GraphQLFieldTypes.INT_REQUIRED ? number :
21
+ T extends GraphQLFieldTypes.INT_OPTIONAL ? number | null :
22
+ T extends GraphQLFieldTypes.BOOLEAN_REQUIRED ? boolean :
23
+ T extends GraphQLFieldTypes.BOOLEAN_OPTIONAL ? boolean | null :
24
+ T extends GraphQLFieldTypes.FLOAT_REQUIRED ? number :
25
+ T extends GraphQLFieldTypes.FLOAT_OPTIONAL ? number | null :
26
+ T extends `[${string}]` | `[${string}]!` ? any[] :
27
+ any;
28
+
29
+ export type ResolverInput<T extends Record<string, GraphQLType>> = {
30
+ [K in keyof T]: TypeFromGraphQL<T[K]>;
31
+ };
32
+
33
+ export function isFieldRequested(info: any, fieldName: string): boolean {
34
+ return info.fieldNodes[0].selectionSet.selections.some((selection: any) =>
35
+ selection.name.value === fieldName
36
+ );
37
+ }
38
+
39
+ export function isFieldRequestedSafe(info: any, ...path: string[]): boolean {
40
+ if (!info || !info.fieldNodes || info.fieldNodes.length === 0) return false;
41
+ const fieldNode = info.fieldNodes[0];
42
+ if (!fieldNode.selectionSet) return false;
43
+ return isPathSelected(fieldNode.selectionSet, path);
44
+ }
45
+
46
+ function isPathSelected(selectionSet: any, path: string[]): boolean {
47
+ if (path.length === 0) return true;
48
+ const [current, ...rest] = path;
49
+ for (const selection of selectionSet.selections) {
50
+ if (selection.kind === 'Field') {
51
+ if (selection.name.value === current) {
52
+ if (rest.length === 0) return true;
53
+ if (selection.selectionSet) {
54
+ return isPathSelected(selection.selectionSet, rest);
55
+ }
56
+ return false;
57
+ }
58
+ } else if (selection.kind === 'InlineFragment' || selection.kind === 'FragmentSpread') {
59
+ // For simplicity, assume fragments are expanded; in practice, they should be resolved
60
+ // Here, we can check if the fragment has the field
61
+ if (selection.selectionSet) {
62
+ if (isPathSelected(selection.selectionSet, path)) return true;
63
+ }
64
+ }
65
+ }
66
+ return false;
67
+ }
package/gql/index.ts CHANGED
@@ -1,12 +1,33 @@
1
- import {createSchema, createYoga} from 'graphql-yoga';
1
+ import {createSchema, createYoga, type Plugin} from 'graphql-yoga';
2
2
  import { GraphQLSchema, GraphQLError } from 'graphql';
3
- import { GraphQLType, GraphQLField, GraphQLOperation } from './Generator';
4
- import {GraphQLTypes} from "./types"
5
- export {
3
+ import { GraphQLObjectType, GraphQLField, GraphQLOperation, GraphQLScalarType } from './Generator';
4
+ import {GraphQLFieldTypes} from "./types"
5
+ import {logger as MainLogger} from "core/Logger"
6
+ import { isFieldRequested } from './helpers';
7
+
8
+ const logger = MainLogger.child({scope: "GQL"});
9
+
10
+ import {
11
+ isValidGraphQLType,
12
+ } from "./helpers";
13
+ import type {
6
14
  GraphQLType,
15
+ TypeFromGraphQL,
16
+ ResolverInput
17
+ } from "./helpers";
18
+ export {
19
+ GraphQLObjectType,
7
20
  GraphQLField,
8
21
  GraphQLOperation,
9
- GraphQLTypes
22
+ GraphQLFieldTypes,
23
+ isValidGraphQLType,
24
+ GraphQLScalarType,
25
+ isFieldRequested
26
+ }
27
+ export type {
28
+ GraphQLType,
29
+ TypeFromGraphQL,
30
+ ResolverInput
10
31
  }
11
32
  interface Entity {
12
33
  id: string;
@@ -49,26 +70,47 @@ const staticResolvers = {
49
70
  }
50
71
  };
51
72
 
52
- export function createYogaInstance(schema?: GraphQLSchema) {
73
+ const maskError = (error: any, message: string): GraphQLError => {
74
+ // Handle authentication errors
75
+ if (error.message === 'Unauthenticated' || error.extensions?.http?.status === 401 || error.extensions?.code === 'UNAUTHENTICATED') {
76
+ return new GraphQLError('Unauthorized', {
77
+ extensions: {
78
+ code: 'UNAUTHORIZED',
79
+ http: { status: 401 }
80
+ }
81
+ });
82
+ }
83
+
84
+ // Handle JWT authentication errors specifically
85
+ if (error.extensions?.code === 'DOWNSTREAM_SERVICE_ERROR' && error.extensions?.http?.status === 401) {
86
+ return new GraphQLError('Unauthorized', {
87
+ extensions: {
88
+ code: 'UNAUTHORIZED',
89
+ http: { status: 401 }
90
+ }
91
+ });
92
+ }
93
+
94
+ if (process.env.NODE_ENV === 'production') {
95
+ logger.error("GraphQL Error:", error);
96
+ // Mask sensitive error details in production
97
+ return new GraphQLError('Internal server error', {
98
+ extensions: {
99
+ code: 'INTERNAL_SERVER_ERROR',
100
+ },
101
+ });
102
+ }
103
+ // In development, return the original error
104
+ return error instanceof GraphQLError ? error : new GraphQLError(message, { originalError: error });
105
+ };
106
+
107
+ export function createYogaInstance(schema?: GraphQLSchema, plugins: Plugin[] = []) {
53
108
  if (schema) {
54
109
  return createYoga({
55
110
  schema,
56
- // Configure error handling to preserve error messages for clients
111
+ plugins,
57
112
  maskedErrors: {
58
- // In development, show full error details
59
- // In production, you might want to mask sensitive information
60
- maskError: (error: any, message: string): GraphQLError => {
61
- if (process.env.NODE_ENV === 'production') {
62
- // Mask sensitive error details in production
63
- return new GraphQLError('Internal server error', {
64
- extensions: {
65
- code: 'INTERNAL_SERVER_ERROR',
66
- },
67
- });
68
- }
69
- // In development, return the original error
70
- return error instanceof GraphQLError ? error : new GraphQLError(message, { originalError: error });
71
- },
113
+ maskError,
72
114
  },
73
115
  });
74
116
  } else {
@@ -77,17 +119,9 @@ export function createYogaInstance(schema?: GraphQLSchema) {
77
119
  typeDefs: staticTypeDefs,
78
120
  resolvers: staticResolvers,
79
121
  }),
122
+ plugins,
80
123
  maskedErrors: {
81
- maskError: (error: any, message: string): GraphQLError => {
82
- if (process.env.NODE_ENV === 'production') {
83
- return new GraphQLError('Internal server error', {
84
- extensions: {
85
- code: 'INTERNAL_SERVER_ERROR',
86
- },
87
- });
88
- }
89
- return error instanceof GraphQLError ? error : new GraphQLError(message, { originalError: error });
90
- },
124
+ maskError,
91
125
  },
92
126
  });
93
127
  }
package/gql/types.ts CHANGED
@@ -7,7 +7,7 @@ export enum GraphQLScalar {
7
7
  Date = "Date",
8
8
  }
9
9
 
10
- export enum GraphQLTypes {
10
+ export enum GraphQLFieldTypes {
11
11
  ID_REQUIRED = "ID!",
12
12
  ID_OPTIONAL = "ID",
13
13
  STRING_REQUIRED = "String!",
package/index.ts CHANGED
@@ -1,22 +1,90 @@
1
- import App from "core/App";
2
- import ServiceRegistry from "service/ServiceRegistry";
3
- import BaseService from "service/Service";
4
- import { Component, CompData, BaseComponent } from "core/Components";
5
- import { Entity } from "core/Entity";
6
- import Query from "core/Query";
7
- import {logger} from "core/Logger";
8
- import { handleGraphQLError, responseError } from "core/ErrorHandler";
1
+ import App from "./core/App";
2
+ import ServiceRegistry from "./service/ServiceRegistry";
3
+ import BaseService from "./service/Service";
4
+ import { Component, CompData, BaseComponent } from "./core/Components";
5
+ import { Entity } from "./core/Entity";
6
+ import ArcheType from "./core/ArcheType";
7
+ import Query from "./core/Query";
8
+ import {logger} from "./core/Logger";
9
+ import { handleGraphQLError, responseError } from "./core/ErrorHandler";
10
+ import { type Plugin } from "graphql-yoga";
11
+ import { BatchLoader } from "core/BatchLoader";
12
+ import { createRequestContextPlugin } from "./core/RequestContext";
13
+ import type { RequestLoaders } from "./core/RequestLoaders";
14
+ // Hook system exports
15
+ import EntityHookManager from "./core/EntityHookManager";
16
+ import {
17
+ EntityHook,
18
+ ComponentHook,
19
+ LifecycleHook,
20
+ ComponentTargetHook,
21
+ registerDecoratedHooks
22
+ } from "./core/decorators/EntityHooks";
23
+ import type {
24
+ EntityHookCallback,
25
+ ComponentHookCallback,
26
+ LifecycleHookCallback,
27
+ HookOptions,
28
+ ComponentTargetConfig
29
+ } from "./core/EntityHookManager";
30
+ import type {
31
+ EntityLifecycleEvent,
32
+ EntityCreatedEvent,
33
+ EntityUpdatedEvent,
34
+ EntityDeletedEvent,
35
+ ComponentLifecycleEvent,
36
+ ComponentAddedEvent,
37
+ ComponentUpdatedEvent,
38
+ ComponentRemovedEvent
39
+ } from "./core/events/EntityLifecycleEvents";
40
+ import { ScheduledTask } from "core/decorators/ScheduledTask";
41
+ import { ScheduleInterval } from "types/scheduler.types";
42
+
9
43
  export {
10
- App,
44
+ App,
45
+ ArcheType,
11
46
  ServiceRegistry,
12
47
  BaseService,
13
48
  BaseComponent,
14
49
  Component,
15
50
  CompData,
16
51
  Entity,
52
+ BatchLoader,
53
+
17
54
  Query,
55
+
56
+ // Scheduler exports
57
+ ScheduleInterval,
58
+ ScheduledTask,
59
+
18
60
  logger,
19
61
 
62
+ type Plugin,
63
+
20
64
  responseError,
21
- handleGraphQLError
22
- };
65
+ handleGraphQLError,
66
+
67
+ createRequestContextPlugin,
68
+ type RequestLoaders,
69
+
70
+ // Hook system exports
71
+ EntityHookManager,
72
+ EntityHook,
73
+ ComponentHook,
74
+ LifecycleHook,
75
+ ComponentTargetHook,
76
+ registerDecoratedHooks,
77
+ type EntityHookCallback,
78
+ type ComponentHookCallback,
79
+ type LifecycleHookCallback,
80
+ type HookOptions,
81
+ type ComponentTargetConfig,
82
+ type EntityLifecycleEvent,
83
+ type EntityCreatedEvent,
84
+ type EntityUpdatedEvent,
85
+ type EntityDeletedEvent,
86
+ type ComponentLifecycleEvent,
87
+ type ComponentAddedEvent,
88
+ type ComponentUpdatedEvent,
89
+ type ComponentRemovedEvent
90
+ };
package/package.json CHANGED
@@ -1,9 +1,17 @@
1
1
  {
2
2
  "name": "bunsane",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "author": {
5
5
  "name": "yaaruu"
6
6
  },
7
+ "keywords": [
8
+ "bun",
9
+ "framework",
10
+ "entity-component-system",
11
+ "ecs",
12
+ "graphql",
13
+ "typescript"
14
+ ],
7
15
  "module": "index.ts",
8
16
  "type": "module",
9
17
  "devDependencies": {
@@ -13,16 +21,17 @@
13
21
  "typescript": "^5"
14
22
  },
15
23
  "dependencies": {
16
- "graphql": "^16.11.0",
17
- "graphql-yoga": "^5.15.1",
18
- "pino": "^9.9.0",
19
- "pino-pretty": "^13.1.1",
20
- "reflect-metadata": "^0.2.2",
21
- "zod": "^4.1.5"
24
+ "dataloader": "2.2.2",
25
+ "graphql": "16.11.0",
26
+ "graphql-yoga": "5.15.1",
27
+ "pino": "9.9.0",
28
+ "pino-pretty": "13.1.1",
29
+ "reflect-metadata": "0.2.2",
30
+ "zod": "4.1.5"
22
31
  },
23
- "graphql": {
24
- "schema": "https://localhost:3001/graphql",
25
- "documents": "**/*.{graphql,js,ts,jsx,tsx}"
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://github.com/yaaruu/bunsane.git"
26
35
  },
27
36
  "license": "MIT"
28
37
  }
@@ -0,0 +1,3 @@
1
+ export function generateRestControllerEndpoints() {
2
+
3
+ }
package/rest/index.ts ADDED
@@ -0,0 +1,22 @@
1
+ export type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS' | 'HEAD';
2
+ export type HTTPEndpointOptions = {
3
+ method: HTTPMethod;
4
+ path: string;
5
+ }
6
+
7
+ export function httpEndpoint(options: HTTPEndpointOptions) {
8
+ return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
9
+ if (!target.constructor.httpEndpoints) {
10
+ target.constructor.httpEndpoints = [];
11
+ }
12
+ target.constructor.httpEndpoints.push({
13
+ method: options.method,
14
+ path: options.path,
15
+ handler: descriptor.value
16
+ });
17
+ }
18
+ }
19
+
20
+ export interface RestController {
21
+ httpEndpoints: Array<{ method: HTTPMethod; path: string; handler: Function }>;
22
+ }
@@ -4,4 +4,4 @@ class BaseService {
4
4
  }
5
5
  }
6
6
 
7
- export default BaseService;
7
+ export default BaseService ;
@@ -6,7 +6,7 @@ import { GraphQLSchema } from "graphql";
6
6
  class ServiceRegistry {
7
7
  static #instance: ServiceRegistry;
8
8
 
9
- private systems: Map<string, BaseService> = new Map();
9
+ private services: Map<string, BaseService> = new Map();
10
10
  private schema: GraphQLSchema | null = null;
11
11
 
12
12
 
@@ -18,8 +18,8 @@ class ServiceRegistry {
18
18
  ApplicationLifecycle.addPhaseListener((event) => {
19
19
  switch(event.detail) {
20
20
  case ApplicationPhase.SYSTEM_REGISTERING: {
21
- const systemsArray = Array.from(this.systems.values());
22
- const { schema } = generateGraphQLSchema(systemsArray);
21
+ const servicesArray = Array.from(this.services.values());
22
+ const { schema } = generateGraphQLSchema(servicesArray);
23
23
  this.schema = schema;
24
24
  ApplicationLifecycle.setPhase(ApplicationPhase.SYSTEM_READY);
25
25
  break;
@@ -37,12 +37,16 @@ class ServiceRegistry {
37
37
  return ServiceRegistry.#instance;
38
38
  }
39
39
 
40
- public registerService(system: BaseService) {
41
- if(!this.systems.has(system.constructor.name)) {
42
- this.systems.set(system.constructor.name, system);
40
+ public registerService(service: BaseService) {
41
+ if(!this.services.has(service.constructor.name)) {
42
+ this.services.set(service.constructor.name, service);
43
43
  }
44
44
  }
45
45
 
46
+ public getServices(): BaseService[] {
47
+ return Array.from(this.services.values());
48
+ }
49
+
46
50
  public getSchema(): GraphQLSchema | null {
47
51
  return this.schema;
48
52
  }
package/service/index.ts CHANGED
@@ -1,6 +1,17 @@
1
1
  import BaseService from "./Service";
2
2
  import { ServiceRegistry } from "index";
3
+ import { httpEndpoint } from "../rest";
4
+
3
5
  export {
4
6
  BaseService,
5
7
  ServiceRegistry
6
- }
8
+ }
9
+
10
+ // Shorthand decorators for HTTP methods
11
+ export const Get = (path: string) => httpEndpoint({ method: 'GET', path });
12
+ export const Post = (path: string) => httpEndpoint({ method: 'POST', path });
13
+ export const Put = (path: string) => httpEndpoint({ method: 'PUT', path });
14
+ export const Delete = (path: string) => httpEndpoint({ method: 'DELETE', path });
15
+ export const Patch = (path: string) => httpEndpoint({ method: 'PATCH', path });
16
+ export const Options = (path: string) => httpEndpoint({ method: 'OPTIONS', path });
17
+ export const Head = (path: string) => httpEndpoint({ method: 'HEAD', path });
@@ -0,0 +1,59 @@
1
+ import {describe, test, expect, beforeAll, beforeEach} from "bun:test"
2
+ import App from "core/App"
3
+ import { BaseComponent, CompData, Component } from "core/Components";
4
+ import { Entity } from "core/Entity";
5
+ import db from "database";
6
+
7
+ let app;
8
+ beforeAll(async () => {
9
+ app = new App();
10
+ await app.waitForAppReady();
11
+ });
12
+
13
+ beforeEach(async () => {
14
+ await db`TRUNCATE TABLE entities CASCADE;`;
15
+ })
16
+
17
+ @Component
18
+ class TestComponent extends BaseComponent {
19
+ @CompData()
20
+ value: string = "";
21
+ }
22
+
23
+ @Component
24
+ class AnotherComponent extends BaseComponent {
25
+ @CompData()
26
+ numberValue: number = 0;
27
+ }
28
+
29
+ @Component
30
+ class YetAnotherComponent extends BaseComponent {
31
+ @CompData()
32
+ boolValue: boolean = false;
33
+ }
34
+
35
+ @Component
36
+ class MassiveComponent extends BaseComponent {
37
+ @CompData()
38
+ value: string = "";
39
+ }
40
+
41
+ describe('Insert Entity Tests', () => {
42
+ test('Creating 10000 entities', async () => {
43
+ const entities = [];
44
+ for(let i = 0; i < 10000; i++) {
45
+ const entity = Entity.Create()
46
+ .add(TestComponent, {value: `Test ${i}`})
47
+ .add(AnotherComponent, {numberValue: i})
48
+ .add(YetAnotherComponent, {boolValue: i % 2 === 0})
49
+ .add(MassiveComponent, {value: "x".repeat(1000)});
50
+ entities.push(entity);
51
+ }
52
+ const start = performance.now();
53
+ await Promise.all(entities.map(entity => entity.save()));
54
+ const end = performance.now();
55
+ console.log(`Time taken to create 10000 entities: ${end - start}ms`);
56
+ const countResult :any = await db<{count: number}>`SELECT COUNT(*)::int FROM entities;`;
57
+ expect(countResult[0].count).toBe(10000);
58
+ });
59
+ });