@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.
- package/README.md +143 -0
- package/database/collection.ts +160 -0
- package/database/decorator.ts +172 -0
- package/database/file.ts +93 -0
- package/database/mongodbadapter.ts +1128 -0
- package/database/rest.ts +14 -0
- package/database/schema.ts +160 -0
- package/database/tenant.ts +37 -0
- package/database/workflow.ts +384 -0
- package/index.ts +28 -0
- package/lib/asyncContextStorage.ts +68 -0
- package/lib/define.ts +114 -0
- package/lib/error.ts +21 -0
- package/lib/files.ts +459 -0
- package/lib/middleware.ts +66 -0
- package/lib/routes.ts +44 -0
- package/lib/scripts.ts +47 -0
- package/lib/services.ts +45 -0
- package/lib/sockets.ts +44 -0
- package/lib/workflow.ts +60 -0
- package/package.json +31 -0
- package/server/api.ts +789 -0
- package/server/boot.ts +101 -0
- package/server/config.ts +107 -0
- package/server/env.ts +16 -0
- package/server/hono.ts +176 -0
- package/server/io.ts +15 -0
- package/server/routes.ts +48 -0
- package/server/security.ts +138 -0
- package/tests/api.test.ts +281 -0
- package/tsconfig.json +36 -0
- package/types/activity.d.ts +45 -0
- package/types/api.d.ts +85 -0
- package/types/collection.d.ts +82 -0
- package/types/config.d.ts +55 -0
- package/types/field.d.ts +72 -0
- package/types/file.d.ts +120 -0
- package/types/hook.d.ts +30 -0
- package/types/middleware.d.ts +18 -0
- package/types/mongo.d.ts +61 -0
- package/types/options.d.ts +7 -0
- package/types/rest.d.ts +18 -0
- package/types/route.d.ts +19 -0
- package/types/schema.d.ts +0 -0
- package/types/scripts.d.ts +10 -0
- package/types/service.d.ts +37 -0
- package/types/task.d.ts +12 -0
- package/types/tenant.d.ts +16 -0
- package/types/token.d.ts +14 -0
- package/types/websocket.d.ts +15 -0
- package/types/workflow.d.ts +91 -0
- package/utils/cache.ts +96 -0
- package/utils/crypto.ts +226 -0
- package/utils/func.ts +1037 -0
- 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
|
+
|
package/database/file.ts
ADDED
|
@@ -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
|
+
}
|