@bejibun/core 0.1.72 → 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/CHANGELOG.md CHANGED
@@ -3,6 +3,66 @@ All notable changes to this project will be documented in this file.
3
3
 
4
4
  ---
5
5
 
6
+ ## [v0.2.0](https://github.com/Bejibun-Framework/bejibun-core/compare/v0.1.73...v0.2.0) - 2026-02-02
7
+
8
+ ### 🩹 Fixes
9
+ - Invalid namespace for model - [#15](https://github.com/Bejibun-Framework/bejibun-core/issues/15)
10
+
11
+ ### 📖 Changes
12
+ - Added dynamic model timestamps for `created_at`, `updated_at`, and `deleted_at`
13
+
14
+ #### Breaking Changes :
15
+ - No longer `BaseColumns` on `Model`
16
+
17
+ #### What's New :
18
+ - Epoch Timestamp Trait
19
+ - Queue Jobs
20
+
21
+ #### What is Epoch Timestamp Trait?
22
+ Override default model timestamps for `created_at`, `updated_at`, and `deleted_at` to unix timestamp.
23
+
24
+ Example :
25
+ ```text
26
+ 2026-01-01 12:00:00.000 +0000 -> 1767948151
27
+ ```
28
+
29
+ #### How does Queue work?
30
+ When you dispatch job, system will add the job to database and save its params.
31
+
32
+ Then worker will run jobs on the database and called `handle` function with params from the database which saved before.
33
+
34
+ Worker only run jobs when the job is available by using `available_at`. You can also delay the job by passing `.delay(seconds)`
35
+
36
+ Example :
37
+ ```ts
38
+ // Immediately
39
+ await TestJob.dispatch(/*any params here*/).send();
40
+
41
+ // With delay
42
+ await TestJob.dispatch(/*any params here*/).delay(60 * 10 /*10 minutes*/).send();
43
+ ```
44
+
45
+ ### ❤️Contributors
46
+ - Havea Crenata ([@crenata](https://github.com/crenata))
47
+
48
+ **Full Changelog**: https://github.com/Bejibun-Framework/bejibun-core/blob/master/CHANGELOG.md
49
+
50
+ ---
51
+
52
+ ## [v0.1.73](https://github.com/Bejibun-Framework/bejibun-core/compare/v0.1.72...v0.1.73) - 2026-02-02
53
+
54
+ ### 🩹 Fixes
55
+ - Fix infinite nested router - [#14](https://github.com/Bejibun-Framework/bejibun-core/pull/14)
56
+
57
+ ### 📖 Changes
58
+
59
+ ### ❤️Contributors
60
+ - Havea Crenata ([@crenata](https://github.com/crenata))
61
+
62
+ **Full Changelog**: https://github.com/Bejibun-Framework/bejibun-core/blob/master/CHANGELOG.md
63
+
64
+ ---
65
+
6
66
  ## [v0.1.72](https://github.com/Bejibun-Framework/bejibun-core/compare/v0.1.71...v0.1.72) - 2026-01-29
7
67
 
8
68
  ### 🩹 Fixes
@@ -67,7 +127,7 @@ All notable changes to this project will be documented in this file.
67
127
  ### 📖 Changes
68
128
  #### What's New :
69
129
  - Added `Router.resource()`
70
-
130
+
71
131
  Single line code that automatically generates a full set of CRUD.
72
132
 
73
133
  #### How to use?
package/README.md CHANGED
@@ -274,22 +274,18 @@ Database table model
274
274
  Example :
275
275
 
276
276
  ```ts
277
- import BaseModel, {BaseColumns} from "@bejibun/core/bases/BaseModel";
278
- import {DateTime} from "luxon";
277
+ import type {Timestamp, NullableTimestamp} from "@bejibun/core/bases/BaseModel";
278
+ import BaseModel from "@bejibun/core/bases/BaseModel";
279
279
 
280
- export interface TestColumns extends BaseColumns {
281
- name: string;
282
- }
283
-
284
- export default class TestModel extends BaseModel implements TestColumns {
280
+ export default class TestModel extends BaseModel {
285
281
  public static tableName: string = "tests";
286
282
  public static idColumn: string = "id";
287
283
 
288
284
  declare id: bigint;
289
285
  declare name: string;
290
- declare created_at: DateTime | string;
291
- declare updated_at: DateTime | string;
292
- declare deleted_at: DateTime | string | null;
286
+ declare created_at: Timestamp;
287
+ declare updated_at: Timestamp;
288
+ declare deleted_at: NullableTimestamp;
293
289
  }
294
290
  ```
295
291
 
@@ -0,0 +1,7 @@
1
+ import JobBuilder from "../builders/JobBuilder";
2
+ export default class BaseJob {
3
+ protected static _namespace: string;
4
+ static get namespace(): string;
5
+ static setNamespace(namespace: string): void;
6
+ static dispatch(...args: any): JobBuilder;
7
+ }
@@ -0,0 +1,17 @@
1
+ import { isEmpty } from "@bejibun/utils";
2
+ import JobBuilder from "../builders/JobBuilder";
3
+ import RuntimeException from "../exceptions/RuntimeException";
4
+ export default class BaseJob {
5
+ static _namespace;
6
+ static get namespace() {
7
+ if (isEmpty(this._namespace))
8
+ throw new RuntimeException(`Job namespace not registered for [${this.name}].`);
9
+ return this._namespace;
10
+ }
11
+ static setNamespace(namespace) {
12
+ this._namespace = namespace;
13
+ }
14
+ static dispatch(...args) {
15
+ return new JobBuilder().setQueue(this.namespace).dispatch(...args);
16
+ }
17
+ }
@@ -1,27 +1,24 @@
1
- import { DateTime } from "luxon";
1
+ import Luxon from "@bejibun/utils/facades/Luxon";
2
2
  import { Constructor, Model, ModelOptions, PartialModelObject, QueryBuilder, QueryBuilderType, QueryContext, TransactionOrKnex } from "objection";
3
3
  import SoftDeletes from "../facades/SoftDeletes";
4
- export interface BaseColumns {
5
- id: bigint | number;
6
- created_at: DateTime | string;
7
- updated_at: DateTime | string;
8
- deleted_at: DateTime | string | null;
9
- }
4
+ export type Timestamp = typeof Luxon.DateTime | Date | string;
5
+ export type NullableTimestamp = Timestamp | null;
10
6
  declare class BunQueryBuilder<M extends Model, R = M[]> extends SoftDeletes<M, R> {
11
7
  update(payload: PartialModelObject<M>): Promise<QueryBuilder<M, R>>;
12
8
  }
13
- export default class BaseModel extends Model implements BaseColumns {
9
+ export default class BaseModel extends Model {
10
+ protected static _namespace: string;
14
11
  static tableName: string;
15
12
  static idColumn: string;
13
+ static createdColumn: string;
14
+ static updatedColumn: string;
16
15
  static deletedColumn: string;
17
16
  static QueryBuilder: typeof BunQueryBuilder;
18
17
  id: number | bigint;
19
- created_at: DateTime | string;
20
- updated_at: DateTime | string;
21
- deleted_at: DateTime | string | null;
22
18
  static get namespace(): string;
23
19
  $beforeInsert(queryContext: QueryContext): void;
24
20
  $beforeUpdate(opt: ModelOptions, queryContext: QueryContext): void;
21
+ static setNamespace(namespace: string): void;
25
22
  static query<T extends Model>(this: Constructor<T>, trxOrKnex?: TransactionOrKnex): QueryBuilderType<T>;
26
23
  static withTrashed<T extends Model>(this: T): QueryBuilderType<T>;
27
24
  static onlyTrashed<T extends Model>(this: T): QueryBuilderType<T>;
@@ -1,12 +1,9 @@
1
- import App from "@bejibun/app";
2
- import { defineValue, isEmpty } from "@bejibun/utils";
3
- import Str from "@bejibun/utils/facades/Str";
4
- import { DateTime } from "luxon";
1
+ import { defineValue, isEmpty, isNotEmpty } from "@bejibun/utils";
2
+ import Luxon from "@bejibun/utils/facades/Luxon";
5
3
  import { Model } from "objection";
6
- import { relative, sep } from "path";
7
- import { fileURLToPath } from "url";
8
4
  import ModelNotFoundException from "../exceptions/ModelNotFoundException";
9
5
  import SoftDeletes from "../facades/SoftDeletes";
6
+ import RuntimeException from "../exceptions/RuntimeException";
10
7
  class BunQueryBuilder extends SoftDeletes {
11
8
  // @ts-ignore
12
9
  async update(payload) {
@@ -20,26 +17,34 @@ class BunQueryBuilder extends SoftDeletes {
20
17
  }
21
18
  // @ts-ignore
22
19
  export default class BaseModel extends Model {
20
+ static _namespace;
23
21
  static tableName;
24
22
  static idColumn;
23
+ static createdColumn = "created_at";
24
+ static updatedColumn = "updated_at";
25
25
  static deletedColumn = "deleted_at";
26
26
  static QueryBuilder = BunQueryBuilder;
27
27
  static get namespace() {
28
- const filePath = fileURLToPath(import.meta.url);
29
- const rel = relative(App.Path.rootPath(), filePath);
30
- const withoutExt = rel.replace(/\.[tj]s$/, "");
31
- const namespaces = withoutExt.split(sep);
32
- namespaces.pop();
33
- namespaces.push(this.name);
34
- return namespaces.map(part => Str.toPascalCase(part)).join("/");
28
+ if (isEmpty(this._namespace))
29
+ throw new RuntimeException(`Model namespace not registered for [${this.name}]`);
30
+ return this._namespace;
35
31
  }
36
32
  $beforeInsert(queryContext) {
37
- const now = DateTime.now();
38
- this.created_at = now;
39
- this.updated_at = now;
33
+ const now = Luxon.DateTime.now();
34
+ if (isNotEmpty(this[this.constructor.createdColumn])) {
35
+ this[this.constructor.createdColumn] = now;
36
+ }
37
+ if (isNotEmpty(this[this.constructor.updatedColumn])) {
38
+ this[this.constructor.updatedColumn] = now;
39
+ }
40
40
  }
41
41
  $beforeUpdate(opt, queryContext) {
42
- this.updated_at = DateTime.now();
42
+ if (isNotEmpty(this[this.constructor.updatedColumn])) {
43
+ this[this.constructor.updatedColumn] = Luxon.DateTime.now();
44
+ }
45
+ }
46
+ static setNamespace(namespace) {
47
+ this._namespace = namespace;
43
48
  }
44
49
  static query(trxOrKnex) {
45
50
  return super.query(trxOrKnex);
package/bases/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from "../bases/BaseController";
2
+ export * from "../bases/BaseJob";
2
3
  export * from "../bases/BaseModel";
3
4
  export * from "../bases/BaseValidator";
package/bases/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from "../bases/BaseController";
2
+ export * from "../bases/BaseJob";
2
3
  export * from "../bases/BaseModel";
3
4
  export * from "../bases/BaseValidator";
package/bootstrap.js CHANGED
@@ -1,3 +1,7 @@
1
+ import App from "@bejibun/app";
1
2
  import Database from "@bejibun/database";
2
3
  import BaseModel from "./bases/BaseModel";
4
+ import NamespaceLoader from "./loader/NamespaceLoader";
3
5
  BaseModel.knex(Database.knex());
6
+ await NamespaceLoader.load(App.Path.jobsPath());
7
+ await NamespaceLoader.load(App.Path.modelsPath());
@@ -0,0 +1,11 @@
1
+ export default class JobBuilder {
2
+ protected queue?: string;
3
+ protected now: number;
4
+ protected availableAt: number;
5
+ protected args: Array<any>;
6
+ constructor();
7
+ setQueue(queue: string): JobBuilder;
8
+ dispatch(...args: any): JobBuilder;
9
+ delay(delay: number): JobBuilder;
10
+ send(): Promise<void>;
11
+ }
@@ -0,0 +1,33 @@
1
+ import Luxon from "@bejibun/utils/facades/Luxon";
2
+ import JobModel from "../models/JobModel";
3
+ export default class JobBuilder {
4
+ queue;
5
+ now;
6
+ availableAt;
7
+ args;
8
+ constructor() {
9
+ this.now = Luxon.DateTime.now().toUnixInteger();
10
+ this.availableAt = this.now;
11
+ this.args = [];
12
+ }
13
+ setQueue(queue) {
14
+ this.queue = queue;
15
+ return this;
16
+ }
17
+ dispatch(...args) {
18
+ this.args.push(...args);
19
+ return this;
20
+ }
21
+ delay(delay) {
22
+ this.availableAt = this.now + delay;
23
+ return this;
24
+ }
25
+ async send() {
26
+ await JobModel.create({
27
+ queue: this.queue,
28
+ payload: JSON.stringify(this.args),
29
+ available_at: this.availableAt,
30
+ created_at: this.now
31
+ });
32
+ }
33
+ }
@@ -0,0 +1,5 @@
1
+ export default class NamespaceBuilder {
2
+ private computeNamespace;
3
+ private walk;
4
+ load(directory: string): Promise<void>;
5
+ }
@@ -0,0 +1,37 @@
1
+ import App from "@bejibun/app";
2
+ import { isEmpty } from "@bejibun/utils";
3
+ import { readdirSync } from "fs";
4
+ import { join, relative, sep } from "path";
5
+ import { pathToFileURL } from "url";
6
+ export default class NamespaceBuilder {
7
+ computeNamespace(filePath) {
8
+ const rel = relative(App.Path.rootPath(), filePath);
9
+ const withoutExt = rel.replace(/\.[tj]s$/, "");
10
+ const parts = withoutExt.split(sep);
11
+ return parts.join("/");
12
+ }
13
+ async walk(directory) {
14
+ const entries = readdirSync(directory, { withFileTypes: true });
15
+ const files = await Promise.all(entries.map((entry) => {
16
+ const fullPath = join(directory, entry.name);
17
+ return entry.isDirectory() ?
18
+ this.walk(fullPath) :
19
+ ((fullPath.endsWith(".ts") ||
20
+ fullPath.endsWith(".js")) ? [fullPath] : []);
21
+ }));
22
+ return files.flat();
23
+ }
24
+ async load(directory) {
25
+ const files = await this.walk(directory);
26
+ for (const file of files) {
27
+ const fileUrl = pathToFileURL(file).href;
28
+ const module = await import(fileUrl);
29
+ const Class = module.default;
30
+ if (isEmpty(Class))
31
+ continue;
32
+ const namespace = this.computeNamespace(file);
33
+ if (typeof Class.setNamespace === "function")
34
+ Class.setNamespace(namespace);
35
+ }
36
+ }
37
+ }
@@ -1,6 +1,6 @@
1
1
  import type { TFacilitator, TPaywall, TX402Config } from "@bejibun/x402";
2
2
  import type { IMiddleware } from "../types/middleware";
3
- import type { HandlerType, ResourceAction, Route, RouterGroup } from "../types/router";
3
+ import type { HandlerType, RawsRoute, ResourceAction, Route, RouterGroup } from "../types/router";
4
4
  import HttpMethodEnum from "@bejibun/utils/enums/HttpMethodEnum";
5
5
  import BaseController from "../bases/BaseController";
6
6
  export interface ResourceOptions {
@@ -8,9 +8,9 @@ export interface ResourceOptions {
8
8
  except?: Array<ResourceAction>;
9
9
  }
10
10
  export default class RouterBuilder {
11
- private basePath;
12
- private middlewares;
13
- private baseNamespace;
11
+ protected basePath: string;
12
+ protected middlewares: Array<IMiddleware>;
13
+ protected baseNamespace: string;
14
14
  prefix(basePath: string): RouterBuilder;
15
15
  middleware(...middlewares: Array<IMiddleware>): RouterBuilder;
16
16
  namespace(baseNamespace: string): RouterBuilder;
@@ -29,12 +29,13 @@ export default class RouterBuilder {
29
29
  trace(path: string, handler: string | HandlerType): Route;
30
30
  match(methods: Array<HttpMethodEnum>, path: string, handler: string | HandlerType): RouterGroup;
31
31
  any(path: string, handler: string | HandlerType): RouterGroup;
32
- serialize(routes: Route | Array<Route> | RouterGroup | Array<RouterGroup>): RouterGroup;
32
+ serialize(routes: Route | Array<Route> | RouterGroup | Array<RouterGroup> | Array<RawsRoute>): RouterGroup;
33
33
  private mergeRoutes;
34
34
  private joinPaths;
35
35
  private resolveControllerString;
36
36
  private resolveIncludedActions;
37
37
  private hasRaw;
38
+ private hasRaws;
38
39
  private isMethodMap;
39
40
  private applyGroup;
40
41
  }
@@ -3,7 +3,6 @@ import Logger from "@bejibun/logger";
3
3
  import { defineValue, isEmpty, isModuleExists, isNotEmpty } from "@bejibun/utils";
4
4
  import HttpMethodEnum from "@bejibun/utils/enums/HttpMethodEnum";
5
5
  import Enum from "@bejibun/utils/facades/Enum";
6
- import path from "path";
7
6
  import RouterException from "../exceptions/RouterException";
8
7
  export default class RouterBuilder {
9
8
  basePath = "";
@@ -31,9 +30,36 @@ export default class RouterBuilder {
31
30
  group(routes) {
32
31
  const rawGroups = [];
33
32
  let routeGroups = {};
33
+ if (this.hasRaws(routes)) {
34
+ const routeList = Array.isArray(routes) ? routes.flat() : [routes];
35
+ const routerGroups = routeList.filter((value) => !this.hasRaws(value) && !this.hasRaw(value));
36
+ const rawRoutes = routeList.filter((value) => this.hasRaws(value)).map((value) => value.raws).flat();
37
+ const newRoutes = {};
38
+ for (const route of rawRoutes) {
39
+ const middlewares = this.middlewares.concat(defineValue(route.raw.middlewares, []));
40
+ const effectiveNamespace = defineValue(this.baseNamespace, route.raw.namespace);
41
+ const cleanPath = this.joinPaths(defineValue(route.raw.prefix, this.basePath), route.raw.path);
42
+ let resolvedHandler = typeof route.raw.handler === "string" ?
43
+ this.resolveControllerString(route.raw.handler, effectiveNamespace) :
44
+ route.raw.handler;
45
+ for (const middleware of [...middlewares].reverse()) {
46
+ resolvedHandler = middleware.handle(resolvedHandler);
47
+ }
48
+ if (isEmpty(newRoutes[cleanPath]))
49
+ newRoutes[cleanPath] = {};
50
+ Object.assign(newRoutes[cleanPath], {
51
+ [route.raw.method]: resolvedHandler
52
+ });
53
+ route.raw.middlewares = middlewares;
54
+ route.raw.namespace = effectiveNamespace;
55
+ route.raw.path = cleanPath;
56
+ rawGroups.push(route);
57
+ }
58
+ routeGroups = Object.assign({}, ...routerGroups.map((value) => this.applyGroup(value)), newRoutes);
59
+ }
34
60
  if (this.hasRaw(routes)) {
35
61
  const routeList = Array.isArray(routes) ? routes.flat() : [routes];
36
- const routerGroups = routeList.filter((value) => !this.hasRaw(value));
62
+ const routerGroups = routeList.filter((value) => !this.hasRaws(value) && !this.hasRaw(value));
37
63
  const rawRoutes = routeList.filter((value) => this.hasRaw(value));
38
64
  const newRoutes = {};
39
65
  for (const route of rawRoutes) {
@@ -196,13 +222,15 @@ export default class RouterBuilder {
196
222
  if (Array.isArray(routes)) {
197
223
  if (this.hasRaw(routes))
198
224
  routes = routes.map((value) => value.route);
225
+ if (this.hasRaws(routes))
226
+ routes = routes.map((value) => value.routes).flat();
199
227
  }
200
228
  else {
201
229
  if (this.hasRaw(routes))
202
230
  routes = routes.route;
231
+ if (this.hasRaws(routes))
232
+ routes = routes.routes;
203
233
  }
204
- if ("raws" in routes)
205
- routes = routes.routes;
206
234
  const mergedRoutes = this.mergeRoutes(routes);
207
235
  if (Array.isArray(mergedRoutes))
208
236
  return Object.assign({}, ...mergedRoutes);
@@ -238,7 +266,7 @@ export default class RouterBuilder {
238
266
  if (isEmpty(controllerName) || isEmpty(methodName)) {
239
267
  throw new RouterException(`Invalid router controller definition: ${definition}.`);
240
268
  }
241
- const controllerPath = path.resolve(App.Path.rootPath(), defineValue(overrideNamespace, this.baseNamespace));
269
+ const controllerPath = App.Path.rootPath(defineValue(overrideNamespace, this.baseNamespace));
242
270
  let location = null;
243
271
  try {
244
272
  location = Bun.resolveSync(`./${controllerName}.ts`, controllerPath);
@@ -289,10 +317,17 @@ export default class RouterBuilder {
289
317
  typeof routes === "object" &&
290
318
  "raw" in routes);
291
319
  }
320
+ hasRaws(routes) {
321
+ if (Array.isArray(routes))
322
+ return routes.flat().some(route => isNotEmpty(route) && "raws" in route);
323
+ return (isNotEmpty(routes) &&
324
+ typeof routes === "object" &&
325
+ "raws" in routes);
326
+ }
292
327
  isMethodMap(value) {
293
328
  return (isNotEmpty(value) &&
294
329
  typeof value === "object" &&
295
- Object.values(value).every(v => typeof v === "function"));
330
+ Object.values(value).every((v) => typeof v === "function"));
296
331
  }
297
332
  applyGroup(route) {
298
333
  if (isEmpty(route))
@@ -301,6 +336,7 @@ export default class RouterBuilder {
301
336
  const routeList = Array.isArray(route) ? route.flat() : [route];
302
337
  const rawRoutes = routeList.filter((value) => this.hasRaw(value));
303
338
  const newRoutes = {};
339
+ const rawGroups = [];
304
340
  for (const route of rawRoutes) {
305
341
  const middlewares = route.raw.middlewares.concat(defineValue(this.middlewares, []));
306
342
  const cleanPath = this.joinPaths(defineValue(route.raw.prefix, this.basePath), route.raw.path);
@@ -318,7 +354,16 @@ export default class RouterBuilder {
318
354
  Object.assign(newRoutes[cleanPath], {
319
355
  [route.raw.method]: resolvedHandler
320
356
  });
357
+ route.raw.middlewares = middlewares;
358
+ route.raw.namespace = effectiveNamespace;
359
+ route.raw.path = cleanPath;
360
+ rawGroups.push(route);
321
361
  }
362
+ if (isNotEmpty(rawGroups))
363
+ return {
364
+ raws: rawGroups,
365
+ routes: newRoutes
366
+ };
322
367
  return newRoutes;
323
368
  }
324
369
  const result = {};
@@ -0,0 +1,27 @@
1
+ export default class MakeJobCommand {
2
+ /**
3
+ * The name and signature of the console command.
4
+ *
5
+ * @var $signature string
6
+ */
7
+ protected $signature: string;
8
+ /**
9
+ * The console command description.
10
+ *
11
+ * @var $description string
12
+ */
13
+ protected $description: string;
14
+ /**
15
+ * The options or optional flag of the console command.
16
+ *
17
+ * @var $options Array<Array<any>>
18
+ */
19
+ protected $options: Array<Array<any>>;
20
+ /**
21
+ * The arguments of the console command.
22
+ *
23
+ * @var $arguments Array<Array<string>>
24
+ */
25
+ protected $arguments: Array<Array<string>>;
26
+ handle(options: any, args: string): Promise<void>;
27
+ }
@@ -0,0 +1,51 @@
1
+ import App from "@bejibun/app";
2
+ import Logger from "@bejibun/logger";
3
+ import { isEmpty } from "@bejibun/utils";
4
+ import Str from "@bejibun/utils/facades/Str";
5
+ import path from "path";
6
+ export default class MakeJobCommand {
7
+ /**
8
+ * The name and signature of the console command.
9
+ *
10
+ * @var $signature string
11
+ */
12
+ $signature = "make:job";
13
+ /**
14
+ * The console command description.
15
+ *
16
+ * @var $description string
17
+ */
18
+ $description = "Create a new job file";
19
+ /**
20
+ * The options or optional flag of the console command.
21
+ *
22
+ * @var $options Array<Array<any>>
23
+ */
24
+ $options = [];
25
+ /**
26
+ * The arguments of the console command.
27
+ *
28
+ * @var $arguments Array<Array<string>>
29
+ */
30
+ $arguments = [
31
+ ["<file>", "The name of the job file"]
32
+ ];
33
+ async handle(options, args) {
34
+ if (isEmpty(args)) {
35
+ Logger.setContext("APP").error("There is no filename provided.");
36
+ return;
37
+ }
38
+ const file = args;
39
+ const jobsDirectory = "jobs";
40
+ const template = Bun.file(path.resolve(__dirname, `../../stubs/${jobsDirectory}/TemplateJob.ts`));
41
+ if (!await template.exists()) {
42
+ Logger.setContext("APP").error("Whoops, something went wrong, the job template not found.");
43
+ return;
44
+ }
45
+ const name = Str.toPascalCase(file.replace(/\s+/g, "").replace(/job/gi, ""));
46
+ const destination = `${name}Job.ts`;
47
+ const content = await template.text();
48
+ await Bun.write(App.Path.jobsPath(destination), content.replace(/template/gi, name));
49
+ Logger.setContext("APP").info(`Job [app/jobs/${destination}] created successfully.`);
50
+ }
51
+ }
@@ -0,0 +1,27 @@
1
+ export default class QueueWorkCommand {
2
+ /**
3
+ * The name and signature of the console command.
4
+ *
5
+ * @var $signature string
6
+ */
7
+ protected $signature: string;
8
+ /**
9
+ * The console command description.
10
+ *
11
+ * @var $description string
12
+ */
13
+ protected $description: string;
14
+ /**
15
+ * The options or optional flag of the console command.
16
+ *
17
+ * @var $options Array<Array<any>>
18
+ */
19
+ protected $options: Array<Array<any>>;
20
+ /**
21
+ * The arguments of the console command.
22
+ *
23
+ * @var $arguments Array<Array<string>>
24
+ */
25
+ protected $arguments: Array<Array<string>>;
26
+ handle(options: any, args: string): Promise<void>;
27
+ }
@@ -0,0 +1,72 @@
1
+ import App from "@bejibun/app";
2
+ import Logger from "@bejibun/logger";
3
+ import { defineValue, isEmpty, isNotEmpty } from "@bejibun/utils";
4
+ import RuntimeException from "../../exceptions/RuntimeException";
5
+ import JobModel from "../../models/JobModel";
6
+ export default class QueueWorkCommand {
7
+ /**
8
+ * The name and signature of the console command.
9
+ *
10
+ * @var $signature string
11
+ */
12
+ $signature = "queue:work";
13
+ /**
14
+ * The console command description.
15
+ *
16
+ * @var $description string
17
+ */
18
+ $description = "Start processing jobs on the queue as a daemon";
19
+ /**
20
+ * The options or optional flag of the console command.
21
+ *
22
+ * @var $options Array<Array<any>>
23
+ */
24
+ $options = [];
25
+ /**
26
+ * The arguments of the console command.
27
+ *
28
+ * @var $arguments Array<Array<string>>
29
+ */
30
+ $arguments = [];
31
+ async handle(options, args) {
32
+ await import(App.Path.rootPath("bootstrap.ts"));
33
+ let running = true;
34
+ process.on("exit", async () => {
35
+ running = false;
36
+ Logger.setContext("Queue").info("Queue worker stopped.");
37
+ });
38
+ process.on("SIGINT", async () => {
39
+ running = false;
40
+ Logger.setContext("Queue").info("Stopping queue worker, SIGINT sent.");
41
+ });
42
+ process.on("SIGTERM", async () => {
43
+ running = false;
44
+ Logger.setContext("Queue").info("Stopping queue worker, SIGTERM sent.");
45
+ });
46
+ while (running) {
47
+ const job = await JobModel.query().where("attempts", "<", 3).orderBy("id", "asc").first();
48
+ if (isNotEmpty(job?.id)) {
49
+ const handler = async () => {
50
+ const module = await import(App.Path.rootPath(job.queue));
51
+ const Class = module.default;
52
+ if (isEmpty(Class))
53
+ throw new RuntimeException(`Job class not found [${job.queue}].`);
54
+ const instance = new Class();
55
+ if (typeof instance.handle !== "function")
56
+ throw new RuntimeException(`Job class has no handle function in [${job.queue}].`);
57
+ instance.handle(JSON.parse(job.payload));
58
+ };
59
+ try {
60
+ await handler();
61
+ await JobModel.query().findById(job.id).delete();
62
+ }
63
+ catch {
64
+ await JobModel.query().findById(job.id).update({
65
+ attempts: defineValue(Number(job.attempts), 0) + 1
66
+ });
67
+ }
68
+ }
69
+ }
70
+ process.exit(0);
71
+ }
72
+ }
package/index.d.ts CHANGED
@@ -3,3 +3,4 @@ export * from "./enums/index";
3
3
  export * from "./exceptions/index";
4
4
  export * from "./facades/index";
5
5
  export * from "./middlewares/index";
6
+ export * from "./models/index";
package/index.js CHANGED
@@ -3,3 +3,4 @@ export * from "./enums/index";
3
3
  export * from "./exceptions/index";
4
4
  export * from "./facades/index";
5
5
  export * from "./middlewares/index";
6
+ export * from "./models/index";
@@ -0,0 +1,3 @@
1
+ export default class NamespaceLoader {
2
+ static load(directory: string): Promise<void>;
3
+ }
@@ -0,0 +1,6 @@
1
+ import NamespaceBuilder from "../builders/NamespaceBuilder";
2
+ export default class NamespaceLoader {
3
+ static async load(directory) {
4
+ return await new NamespaceBuilder().load(directory);
5
+ }
6
+ }
@@ -0,0 +1,2 @@
1
+ declare const EpochTimestamps: (Base: any) => any;
2
+ export default EpochTimestamps;
@@ -0,0 +1,20 @@
1
+ import { isNotEmpty } from "@bejibun/utils";
2
+ import Luxon from "@bejibun/utils/facades/Luxon";
3
+ const EpochTimestamps = (Base) => class extends Base {
4
+ $beforeInsert(queryContext) {
5
+ const now = Luxon.DateTime.now().toUnixInteger();
6
+ if (isNotEmpty(this[this.constructor.createdColumn])) {
7
+ this[this.constructor.createdColumn] = now;
8
+ }
9
+ if (isNotEmpty(this[this.constructor.updatedColumn])) {
10
+ this[this.constructor.updatedColumn] = now;
11
+ }
12
+ }
13
+ $beforeUpdate(opt, queryContext) {
14
+ this.updated_at = Luxon.DateTime.now().toUnixInteger();
15
+ if (isNotEmpty(this[this.constructor.updatedColumn])) {
16
+ this[this.constructor.updatedColumn] = Luxon.DateTime.now().toUnixInteger();
17
+ }
18
+ }
19
+ };
20
+ export default EpochTimestamps;
@@ -0,0 +1,18 @@
1
+ import { ModelOptions, QueryBuilder, QueryContext } from "objection";
2
+ declare const JobModel_base: any;
3
+ export default class JobModel extends JobModel_base {
4
+ static tableName: string;
5
+ static idColumn: string;
6
+ static updatedColumn: null;
7
+ static deletedColumn: null;
8
+ static QueryBuilder: typeof QueryBuilder;
9
+ $beforeUpdate(opt: ModelOptions, queryContext: QueryContext): void;
10
+ id: bigint;
11
+ queue: string;
12
+ payload: string;
13
+ attempts: bigint;
14
+ reserved_at: bigint | null;
15
+ available_at: bigint;
16
+ created_at: bigint;
17
+ }
18
+ export {};
@@ -0,0 +1,13 @@
1
+ import { QueryBuilder } from "objection";
2
+ import BaseModel from "../bases/BaseModel";
3
+ import EpochTimestamps from "../models/EpochTimestamps";
4
+ export default class JobModel extends EpochTimestamps(BaseModel) {
5
+ static tableName = "jobs";
6
+ static idColumn = "id";
7
+ static updatedColumn = null;
8
+ static deletedColumn = null;
9
+ static QueryBuilder = QueryBuilder;
10
+ $beforeUpdate(opt, queryContext) {
11
+ // do nothing
12
+ }
13
+ }
@@ -0,0 +1 @@
1
+ export * from "../models/JobModel";
@@ -0,0 +1 @@
1
+ export * from "../models/JobModel";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bejibun/core",
3
- "version": "0.1.72",
3
+ "version": "0.2.0",
4
4
  "author": "Havea Crenata <havea.crenata@gmail.com>",
5
5
  "repository": {
6
6
  "type": "git",
@@ -15,8 +15,8 @@
15
15
  "@bejibun/database": "^0.1.19",
16
16
  "@bejibun/logger": "^0.1.22",
17
17
  "@bejibun/utils": "^0.1.28",
18
- "@vinejs/vine": "^4.2.0",
19
- "commander": "^14.0.2",
18
+ "@vinejs/vine": "^4.3.0",
19
+ "commander": "^14.0.3",
20
20
  "luxon": "^3.7.2",
21
21
  "objection": "^3.1.5"
22
22
  },
@@ -0,0 +1,12 @@
1
+ import BaseJob from "@bejibun/core/bases/BaseJob";
2
+
3
+ export default class TemplateJob extends BaseJob {
4
+ /**
5
+ * Execute the job.
6
+ *
7
+ * @var $arguments Array<any>
8
+ */
9
+ public async handle(args: Array<any>): Promise<void> {
10
+ //
11
+ }
12
+ }
@@ -1,17 +1,13 @@
1
- import BaseModel, {BaseColumns} from "@bejibun/core/bases/BaseModel";
2
- import Luxon from "@bejibun/utils/facades/Luxon";
1
+ import type {Timestamp, NullableTimestamp} from "@bejibun/core/bases/BaseModel";
2
+ import BaseModel from "@bejibun/core/bases/BaseModel";
3
3
 
4
- export interface TemplateColumns extends BaseColumns {
5
- name: string;
6
- }
7
-
8
- export default class TemplateModel extends BaseModel implements TemplateColumns {
4
+ export default class TemplateModel extends BaseModel {
9
5
  public static tableName: string = "templates";
10
6
  public static idColumn: string = "id";
11
7
 
12
8
  declare id: bigint;
13
9
  declare name: string;
14
- declare created_at: Luxon.DateTime | string;
15
- declare updated_at: Luxon.DateTime | string;
16
- declare deleted_at: Luxon.DateTime | string | null;
10
+ declare created_at: Timestamp;
11
+ declare updated_at: Timestamp;
12
+ declare deleted_at: NullableTimestamp;
17
13
  }
package/types/router.d.ts CHANGED
@@ -15,4 +15,8 @@ export type Route = {
15
15
  raw: RawRoute,
16
16
  route: RouterGroup
17
17
  };
18
+ export type RawsRoute = {
19
+ raws: Array<Route>,
20
+ routes: Array<RouterGroup>
21
+ };
18
22
  export type ResourceAction = "index" | "store" | "show" | "update" | "destroy";