@anteros/core 0.0.1-alpha.1

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 (55) hide show
  1. package/README.md +143 -0
  2. package/database/collection.ts +160 -0
  3. package/database/decorator.ts +172 -0
  4. package/database/file.ts +93 -0
  5. package/database/mongodbadapter.ts +1128 -0
  6. package/database/rest.ts +14 -0
  7. package/database/schema.ts +160 -0
  8. package/database/tenant.ts +37 -0
  9. package/database/workflow.ts +384 -0
  10. package/index.ts +28 -0
  11. package/lib/asyncContextStorage.ts +68 -0
  12. package/lib/define.ts +114 -0
  13. package/lib/error.ts +21 -0
  14. package/lib/files.ts +459 -0
  15. package/lib/middleware.ts +66 -0
  16. package/lib/routes.ts +44 -0
  17. package/lib/scripts.ts +47 -0
  18. package/lib/services.ts +45 -0
  19. package/lib/sockets.ts +44 -0
  20. package/lib/workflow.ts +60 -0
  21. package/package.json +31 -0
  22. package/server/api.ts +789 -0
  23. package/server/boot.ts +101 -0
  24. package/server/config.ts +107 -0
  25. package/server/env.ts +16 -0
  26. package/server/hono.ts +176 -0
  27. package/server/io.ts +15 -0
  28. package/server/routes.ts +48 -0
  29. package/server/security.ts +138 -0
  30. package/tests/api.test.ts +281 -0
  31. package/tsconfig.json +36 -0
  32. package/types/activity.d.ts +45 -0
  33. package/types/api.d.ts +85 -0
  34. package/types/collection.d.ts +82 -0
  35. package/types/config.d.ts +55 -0
  36. package/types/field.d.ts +72 -0
  37. package/types/file.d.ts +120 -0
  38. package/types/hook.d.ts +30 -0
  39. package/types/middleware.d.ts +18 -0
  40. package/types/mongo.d.ts +61 -0
  41. package/types/options.d.ts +7 -0
  42. package/types/rest.d.ts +18 -0
  43. package/types/route.d.ts +19 -0
  44. package/types/schema.d.ts +0 -0
  45. package/types/scripts.d.ts +10 -0
  46. package/types/service.d.ts +37 -0
  47. package/types/task.d.ts +12 -0
  48. package/types/tenant.d.ts +16 -0
  49. package/types/token.d.ts +14 -0
  50. package/types/websocket.d.ts +15 -0
  51. package/types/workflow.d.ts +91 -0
  52. package/utils/cache.ts +96 -0
  53. package/utils/crypto.ts +226 -0
  54. package/utils/func.ts +1037 -0
  55. package/utils/index.ts +17 -0
package/README.md ADDED
@@ -0,0 +1,143 @@
1
+ # @dnax/core
2
+
3
+ Server core for **multi-tenant** apps: generic **MongoDB** REST API, **Joi** validation, hooks, JWT auth, custom routes, and boot scripts. Built for **Bun** with **Hono** and **Socket.io**.
4
+
5
+ ## Requirements
6
+
7
+ - [Bun](https://bun.sh) (recommended runtime for this package)
8
+ - A reachable MongoDB instance (per-tenant URI)
9
+ - TypeScript (peer dependency)
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ bun add @dnax/core@0.0.0-rc.13
15
+ ```
16
+
17
+ ## Quick start
18
+
19
+ Typical entrypoint in your app (working directory = project root that contains tenant folders):
20
+
21
+ ```ts
22
+ import { app, define } from "@dnax/core";
23
+
24
+ await app.boot({
25
+ clusterMode: false,
26
+ server: {
27
+ port: 5000,
28
+ jwt: {
29
+ secret: process.env.JWT_SECRET, // or JWT_SECRET env var
30
+ expiresIn: "1h",
31
+ },
32
+ cors: {
33
+ origin: ["http://localhost:3000"],
34
+ credentials: true,
35
+ },
36
+ },
37
+ tenants: [
38
+ {
39
+ id: "v1",
40
+ dir: "v1", // relative path to tenant code (collections, routes, scripts)
41
+ routes: { prefix: "/api/v1" }, // required for loading declared routes
42
+ database: {
43
+ uri: "mongodb://localhost:27017/my_database",
44
+ },
45
+ },
46
+ ],
47
+ });
48
+ ```
49
+
50
+ On boot, the server syncs tenants, loads collections (`*.model.ts`), registers routes (`*.route.ts`), starts HTTP/WebSocket, then runs enabled scripts (`*.run.ts`).
51
+
52
+ ## Public exports
53
+
54
+ | Export | Purpose |
55
+ |--------|---------|
56
+ | `define` | Factories: `define.Collection`, `define.Route`, `define.Script`, `define.Server` / `define.App` |
57
+ | `app` | `{ boot }` — starts the application |
58
+ | `useRest` | REST client / per-tenant Mongo access (programmatic use or in handlers) |
59
+ | `v` | **Joi** (re-export), for schemas in models |
60
+ | `utils` | Internal utilities exposed by the package |
61
+
62
+ ## Folder layout (per tenant)
63
+
64
+ **`dir` is required** on every tenant: it is the path (relative to the project root) under which the folders below are resolved.
65
+
66
+ Paths are relative to `tenant.dir` (e.g. `v1/` when `id` and `dir` are `v1`):
67
+
68
+ | Path | Contents |
69
+ |------|----------|
70
+ | `collections/**/*.model.ts` | Collection models (`define.Collection({ ... })`) |
71
+ | `routes/**/*.route.ts` | Custom HTTP routes (`define.Route({ ... })`), requires `tenant.routes.prefix` |
72
+ | `scripts/**/*.run.ts` | Startup scripts (`define.Script({ ... })`) |
73
+
74
+ ## Generic HTTP API
75
+
76
+ A single route handles collection operations:
77
+
78
+ ```http
79
+ POST /api/:tenant_id/:collection/:action
80
+ Content-Type: application/json
81
+ ```
82
+
83
+ Example `action` values: `find`, `findOne`, `insertOne`, `insertMany`, `updateOne`, `updateMany`, `deleteOne`, `deleteMany`, `findOneAndUpdate`, `aggregate`, `login`, `logout`, plus **named actions** declared on the model.
84
+
85
+ Typical JSON bodies: `{ "data": ... }`, `{ "params": ... }`, `{ "pipeline": ... }`, `{ "payload": ... }` (login), depending on the action.
86
+
87
+ The tenant must exist in config; the collection must be registered via a `*.model.ts` file.
88
+
89
+ ### JWT and `Authorization` header
90
+
91
+ If the client sends `Authorization: Bearer <jwt>`, the core **verifies** it before your route handler runs. Invalid or expired tokens return **401** immediately.
92
+
93
+ When verification succeeds, the Hono context exposes:
94
+
95
+ | Field | Meaning |
96
+ |-------|--------|
97
+ | `token.value` | Raw JWT string (same as in the header) |
98
+ | `token.decoded` | Verified payload (claims), e.g. `sub`, `role` |
99
+
100
+ If there is **no** `Authorization` header, `token` is not set on the context. Collection **`api.access`** functions receive:
101
+
102
+ ```ts
103
+ token: {
104
+ value: string | null; // raw JWT, or null if no Bearer was sent
105
+ decoded: Record<string, unknown> | null; // verified claims, or null if unauthenticated
106
+ };
107
+ ```
108
+
109
+ Use **`token.decoded`** for roles, `sub`, etc. Use **`token.value`** if you need the string (e.g. forwarding). Implement rules in `api.access` for each action you want to protect.
110
+
111
+ ### Access control (`api.access`)
112
+
113
+ Per collection, `api.access` allows or denies each action (`boolean` or async function). Wildcard `'*'` applies when no specific rule exists for an action.
114
+
115
+ ### Auth (`api.auth`)
116
+
117
+ When `api.auth.enabled` is set, define `onLogin` / `onLogout` on the model. The JWT secret is read from `server.jwt.secret` or the **`JWT_SECRET`** environment variable.
118
+
119
+ ## Server configuration
120
+
121
+ Common options when calling `app.boot`:
122
+
123
+ - `clusterMode` — Bun `reusePort` for multiple workers
124
+ - `server.port`, `server.name`
125
+ - `server.cors` — origins, credentials, headers, and methods
126
+ - `server.ipRestriction` — allow/deny lists (Hono)
127
+ - `server.jwt` — secret and token lifetime
128
+
129
+ ## Socket.io
130
+
131
+ The server mounts **Socket.io** at `/socket.io/`. Route handlers receive the `io` instance for real-time subscriptions.
132
+
133
+ ## Environment variables
134
+
135
+ | Variable | Usage |
136
+ |----------|--------|
137
+ | `JWT_SECRET` | JWT signing when not set in config |
138
+ | `NODE_ENV` / `Bun.env.NODE_ENV` | Environment label at boot |
139
+ | `APP_NAME` | Server name when `server.name` is omitted |
140
+
141
+ ## License
142
+
143
+ See the parent repository for the project license.
@@ -0,0 +1,160 @@
1
+ import { Glob } from "bun"
2
+ import { cfg } from "../server/config"
3
+ import path from "path"
4
+ import fs from "fs/promises"
5
+ import type { Collection } from "../types/collection"
6
+ import type { IndexDescription } from "mongodb"
7
+ import type { Field } from "../types/field"
8
+ import { getTenant } from "./tenant"
9
+ import { buildSchema } from "./schema"
10
+ import type { FileCollection } from "../types/file"
11
+ async function syncCollections() {
12
+ try {
13
+
14
+ const collections: Collection[] = []
15
+ for (let tenant of cfg.tenants ?? []) {
16
+ const TENANT_PATH = path.join(process.cwd(), tenant.dir)
17
+ const COLLECTIONS_PATH = path.join(TENANT_PATH, 'collections')
18
+ let exist = await fs.exists(COLLECTIONS_PATH)
19
+ if (!exist) continue;
20
+ const isDirectory = await (await fs.stat(COLLECTIONS_PATH)).isDirectory()
21
+
22
+ if (isDirectory) {
23
+ const glob = new Glob(path.join(COLLECTIONS_PATH, '**/*.model.ts'))
24
+ for await (let file of glob.scan('.')) {
25
+ let collectionModule = await import(file)
26
+ if (collectionModule?.default?._isCollection_) {
27
+ collections.push({
28
+ ...collectionModule?.default,
29
+ _tenant_: tenant.id
30
+ })
31
+ }
32
+ }
33
+ }
34
+ }
35
+
36
+ // construction des schema arktype pour les collections
37
+ for (let collection of collections) {
38
+ collection._schema_ = buildSchema(collection)
39
+ collection._schemaPartial_ = buildSchema(collection, { partial: true })
40
+ }
41
+
42
+ initializeOnDatabase(collections).catch(err => {
43
+ console.error('Initialize collections on database failed', err?.message)
44
+ })
45
+ cfg.collections = collections
46
+
47
+ } catch (err: any) {
48
+ console.error(err?.message)
49
+ }
50
+ }
51
+
52
+
53
+
54
+
55
+ async function initializeOnDatabase(collections: Collection[]) {
56
+ for (let collection of collections) {
57
+ try {
58
+ const tenant = getTenant(collection._tenant_!)
59
+ const db = tenant?.database?.db
60
+ if (db && collection) {
61
+
62
+ // create collection on database
63
+ if (!collection?._isTimeSerie_) {
64
+ await db.createCollection(collection.slug)
65
+ }
66
+
67
+ // create default index on collection
68
+ let specsTimeStamps = {
69
+ createdAt: -1,
70
+ updatedAt: -1
71
+ }
72
+ await db.collection(collection.slug).createIndex(specsTimeStamps)
73
+ // console.log('index created on collection', collection.slug, index)
74
+
75
+
76
+
77
+ // creation d'index sur les champs
78
+ let specsFieldsIndexes: IndexDescription[] = []
79
+ for (let field of collection?.fields || []) {
80
+ field.indexOptions = field.indexOptions ?? {}
81
+ if ((field.type == 'random' && Object.keys(field.randomOptions ?? {}).length) || field.unique) {
82
+ field.index = field.index ?? true
83
+ field.indexOptions.unique = true
84
+ }
85
+
86
+ if (field?.index) {
87
+ let indexValue: Boolean | number = field.index;
88
+ let includesStringType: Field['type'][] = ['string', 'email', 'enum', 'url', 'random'];
89
+ let includesDateType: Field['type'][] = ['date', 'datetime-local'];
90
+ if (typeof indexValue == "boolean" && includesStringType.includes(field.type)) {
91
+ indexValue = 1
92
+ }
93
+
94
+ if (typeof indexValue == "boolean" && includesDateType.includes(field.type)) {
95
+ indexValue = -1
96
+ }
97
+ specsFieldsIndexes.push({
98
+ key: {
99
+ [field.name]: indexValue as number
100
+ },
101
+ unique: field.unique ?? false,
102
+ sparse: field.type == 'random' ? true : false,
103
+ ...field.indexOptions,
104
+ })
105
+
106
+ }
107
+
108
+
109
+
110
+ }
111
+ if (specsFieldsIndexes.length) {
112
+ await db.collection(collection.slug).createIndexes(specsFieldsIndexes)
113
+ }
114
+ }
115
+
116
+ } catch (err: any) {
117
+ console.error(err?.message)
118
+
119
+ }
120
+ }
121
+ }
122
+
123
+ function getCollection(collectionName: string, tenantId: string): Collection | null {
124
+
125
+ let col = cfg.collections?.find(collection => collection.slug == collectionName && collection._tenant_ == tenantId)
126
+
127
+ if (col) {
128
+ return col
129
+ }
130
+
131
+ // Chercher aussi dans les file collections
132
+ let fileCol = cfg.fileCollections?.find(fc => fc.slug == collectionName && fc._tenant_ == tenantId)
133
+ if (fileCol) {
134
+ return {
135
+ slug: fileCol.slug,
136
+ type: 'file',
137
+ fields: fileCol.fields ?? [],
138
+ api: fileCol.api as any,
139
+ hooks: fileCol.hooks as any,
140
+ _tenant_: fileCol._tenant_,
141
+ _schema_: fileCol._schema_,
142
+ _schemaPartial_: fileCol._schemaPartial_,
143
+ }
144
+ }
145
+
146
+ return null
147
+ }
148
+
149
+
150
+ function getCollectionKeys(collection: Collection): string[] {
151
+ return collection.fields.map(field => field.name)
152
+ }
153
+
154
+
155
+
156
+ export {
157
+ syncCollections,
158
+ getCollection,
159
+ getCollectionKeys
160
+ }
@@ -0,0 +1,172 @@
1
+ import { ObjectId } from "mongodb";
2
+ import { getCollection } from "./collection";
3
+ import { AppError } from "../lib/error";
4
+
5
+
6
+ // decoration function checkCollectionExists
7
+ export function CheckIfCollectionExists(): any {
8
+ return function (target: any, propertyKey: string, descriptor: PropertyDescriptor): any {
9
+ if (!descriptor || !descriptor.value) {
10
+ return descriptor;
11
+ }
12
+ const originalMethod = descriptor.value;
13
+ descriptor.value = async function (...args: any[]) {
14
+ let collection = args[0];
15
+ let col = getCollection(collection, this.tenant_id)
16
+ if (!col) {
17
+ throw new AppError(`collection '${collection}' not found`, {
18
+ code: 'COLLECTION_NOT_FOUND',
19
+ status: 500
20
+ })
21
+ }
22
+ return await originalMethod.apply(this, args);
23
+ };
24
+ return descriptor;
25
+ }
26
+ }
27
+
28
+
29
+
30
+ export function CheckIfArrayOfIds(action: string): any {
31
+ return function (target: any, propertyKey: string, descriptor: PropertyDescriptor): any {
32
+ if (!descriptor || !descriptor.value) {
33
+ return descriptor;
34
+ }
35
+ const originalMethod = descriptor.value;
36
+ descriptor.value = async function (...args: any[]) {
37
+
38
+ let _ids = args[1];
39
+ if (!Array.isArray(_ids)) {
40
+ throw new AppError(`[${action}] IDs must be an array`, {
41
+ code: 'INVALID_ARGUMENT',
42
+ status: 400
43
+ })
44
+ }
45
+ for (let id of _ids) {
46
+ if (!ObjectId.isValid(id)) {
47
+ throw new AppError(`[${action}] IDs must be an array of valid ObjectId`, {
48
+ code: 'INVALID_ARGUMENT',
49
+ status: 400
50
+ })
51
+ }
52
+ }
53
+ if (_ids.length === 0) {
54
+ throw new AppError(`[${action}] IDs must be an array of at least one valid ObjectId`, {
55
+ code: 'INVALID_ARGUMENT',
56
+ status: 400
57
+ })
58
+ }
59
+ return await originalMethod.apply(this, args);
60
+ }
61
+ return descriptor;
62
+ };
63
+ }
64
+
65
+ export function CheckInsertData(action: string): any {
66
+ return function (target: any, propertyKey: string, descriptor: PropertyDescriptor): any {
67
+ if (!descriptor || !descriptor.value) {
68
+ return descriptor;
69
+ }
70
+ const originalMethod = descriptor.value;
71
+ descriptor.value = async function (...args: any[]) {
72
+ let data = args[1];
73
+ if (action === 'insertOne') {
74
+ if (!data) throw new AppError(`[${action}] Data is required`, {
75
+ code: 'INVALID_ARGUMENT',
76
+ status: 400
77
+ })
78
+ }
79
+ if (action === 'insertMany') {
80
+ if (!data) throw new AppError(`[${action}] Data is required`, {
81
+ code: 'INVALID_ARGUMENT',
82
+ status: 400
83
+ })
84
+ }
85
+ return await originalMethod.apply(this, args);
86
+ }
87
+ }
88
+ }
89
+
90
+ export function CheckIfId(action: string): any {
91
+ return function (target: any, propertyKey: string, descriptor: PropertyDescriptor): any {
92
+ if (!descriptor || !descriptor.value) {
93
+ return descriptor;
94
+ }
95
+ const originalMethod = descriptor.value;
96
+ descriptor.value = async function (...args: any[]) {
97
+ let _id = args[1];
98
+ if (!ObjectId.isValid(_id)) {
99
+ throw new AppError(`[${action}] ID must be a valid ObjectId`, {
100
+ code: 'INVALID_ARGUMENT',
101
+ status: 400
102
+ })
103
+ }
104
+ return await originalMethod.apply(this, args);
105
+ }
106
+ return descriptor;
107
+ }
108
+ }
109
+
110
+
111
+ export function CheckFilter(): any {
112
+ return function (target: any, propertyKey: string, descriptor: PropertyDescriptor): any {
113
+ if (!descriptor || !descriptor.value) {
114
+ return descriptor;
115
+ }
116
+ const originalMethod = descriptor.value;
117
+ descriptor.value = async function (...args: any[]) {
118
+ let filter = args[1];
119
+ if (
120
+ filter === null ||
121
+ filter === undefined ||
122
+ (typeof filter === 'object' && Object.keys(filter).length === 0)
123
+ ) {
124
+ throw new AppError(`[${propertyKey}] Filter is required and must not be empty`, {
125
+ code: 'FILTER_REQUIRED',
126
+ status: 400
127
+ })
128
+ }
129
+ return await originalMethod.apply(this, args);
130
+ }
131
+ return descriptor;
132
+ }
133
+ }
134
+
135
+ export function CheckBulkWriteOperations(): any {
136
+ return function (target: any, propertyKey: string, descriptor: PropertyDescriptor): any {
137
+ if (!descriptor || !descriptor.value) {
138
+ return descriptor;
139
+ }
140
+ const originalMethod = descriptor.value;
141
+ descriptor.value = async function (...args: any[]) {
142
+ let operations = args[1];
143
+ for (let operation of operations) {
144
+ if (!operation) throw new AppError(`[bulkWrite] Operation is required`, {
145
+ code: 'INVALID_ARGUMENT',
146
+ status: 400
147
+ })
148
+ if (Object.hasOwn(operation, 'insertOne')) {
149
+ if (!operation.insertOne.document) throw new AppError(`[bulkWrite] Document is required`, {
150
+ code: 'INVALID_ARGUMENT',
151
+ status: 400
152
+ })
153
+ }
154
+ if (Object.hasOwn(operation, 'updateOne')) {
155
+ if (!operation.updateOne.filter) throw new AppError(`[bulkWrite] Filter is required`, {
156
+ code: 'INVALID_ARGUMENT',
157
+ status: 400
158
+ })
159
+ }
160
+ if (Object.hasOwn(operation, 'updateOne')) {
161
+ if (!operation.updateOne.filter) throw new AppError(`[bulkWrite] Filter is required`, {
162
+ code: 'INVALID_ARGUMENT',
163
+ status: 400
164
+ })
165
+ }
166
+ }
167
+ return await originalMethod.apply(this, args);
168
+ }
169
+ return descriptor;
170
+ }
171
+ }
172
+
@@ -0,0 +1,93 @@
1
+ import { Glob } from "bun"
2
+ import { cfg } from "../server/config"
3
+ import path from "path"
4
+ import fs from "fs/promises"
5
+ import type { FileCollection } from "../types/file"
6
+ import { getTenant } from "./tenant"
7
+
8
+ async function syncFileCollections() {
9
+ try {
10
+ const fileCollections: FileCollection[] = []
11
+ for (let tenant of cfg.tenants ?? []) {
12
+ const TENANT_PATH = path.join(process.cwd(), tenant.dir)
13
+ const FILES_PATH = path.join(TENANT_PATH, 'files')
14
+ let exist = await fs.exists(FILES_PATH)
15
+ if (!exist) continue;
16
+ const isDirectory = await (await fs.stat(FILES_PATH)).isDirectory()
17
+
18
+ if (isDirectory) {
19
+ // Charger les fichiers *.file.ts
20
+ const globFiles = new Glob(path.join(FILES_PATH, '**/*.file.ts'))
21
+ for await (let file of globFiles.scan('.')) {
22
+ let fileModule = await import(file)
23
+ if (fileModule?.default?._isFileCollection_) {
24
+ fileCollections.push({
25
+ ...fileModule?.default,
26
+ _tenant_: tenant.id
27
+ })
28
+ }
29
+ }
30
+
31
+ // Charger aussi les fichiers *.model.ts avec _isFileCollection_
32
+ const globModels = new Glob(path.join(FILES_PATH, '**/*.model.ts'))
33
+ for await (let file of globModels.scan('.')) {
34
+ let fileModule = await import(file)
35
+ if (fileModule?.default?._isFileCollection_) {
36
+ fileCollections.push({
37
+ ...fileModule?.default,
38
+ _tenant_: tenant.id
39
+ })
40
+ }
41
+ }
42
+ }
43
+ }
44
+
45
+ cfg.fileCollections = fileCollections
46
+
47
+ initializeFileCollectionsOnDatabase(fileCollections).catch(err => {
48
+ console.error('Initialize file collections on database failed', err?.message)
49
+ })
50
+
51
+ } catch (err: any) {
52
+ console.error(err?.message)
53
+ }
54
+ }
55
+
56
+ async function initializeFileCollectionsOnDatabase(files: FileCollection[]) {
57
+ for (const fileCollection of files) {
58
+ try {
59
+ const tenant = getTenant(fileCollection._tenant_!)
60
+ const db = tenant?.database?.db
61
+ if (!db) continue;
62
+
63
+ // Créer la collection si elle n'existe pas
64
+ const collections = await db.listCollections({ name: fileCollection.slug }).toArray()
65
+ if (collections.length === 0) {
66
+ await db.createCollection(fileCollection.slug)
67
+ }
68
+
69
+ // Créer les indexes par défaut
70
+ await db.collection(fileCollection.slug).createIndexes([
71
+ { key: { filename: 1 } },
72
+ { key: { name: 1 } },
73
+ { key: { mimetype: 1 } },
74
+ { key: { createdAt: -1, updatedAt: -1 } },
75
+ ])
76
+
77
+ } catch (err: any) {
78
+ console.error(`Initialize file collection '${fileCollection.slug}' failed`, err?.message)
79
+ }
80
+ }
81
+ }
82
+
83
+ function getFileCollection(collectionName: string, tenantId: string): FileCollection | null {
84
+ let col = cfg.fileCollections?.find(c => c.slug == collectionName && c._tenant_ == tenantId)
85
+ if (col) return col
86
+ return null
87
+ }
88
+
89
+ export {
90
+ syncFileCollections,
91
+ getFileCollection,
92
+ initializeFileCollectionsOnDatabase
93
+ }