@datrix/core 0.1.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/LICENSE +21 -0
- package/README.md +281 -0
- package/dist/index.d.mts +1935 -0
- package/dist/index.d.ts +1935 -0
- package/dist/index.js +8938 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +8805 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 datrix Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# @datrix/core
|
|
2
|
+
|
|
3
|
+
The heart of Datrix. Handles schema definition, validation, query building, relation processing, and database migrations — without being tied to a specific database engine.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @datrix/core
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Setup
|
|
12
|
+
|
|
13
|
+
Two functions are the entry point to every Datrix project: `defineSchema` and `defineConfig`.
|
|
14
|
+
|
|
15
|
+
### defineSchema
|
|
16
|
+
|
|
17
|
+
Validates your schema object against `SchemaDefinition` at the TypeScript level. At runtime it returns the object as-is — it is a type helper, not a factory.
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import { defineSchema } from "@datrix/core"
|
|
21
|
+
|
|
22
|
+
export const postSchema = defineSchema({
|
|
23
|
+
name: "post",
|
|
24
|
+
fields: {
|
|
25
|
+
title: { type: "string", required: true },
|
|
26
|
+
status: { type: "enum", values: ["draft", "published"] as const, default: "draft" },
|
|
27
|
+
author: { type: "relation", kind: "belongsTo", model: "user" },
|
|
28
|
+
},
|
|
29
|
+
indexes: [
|
|
30
|
+
{ fields: ["status"] },
|
|
31
|
+
],
|
|
32
|
+
})
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### defineConfig
|
|
36
|
+
|
|
37
|
+
Creates the Datrix instance factory. Calling the returned function initializes the instance once — subsequent calls return the cached instance.
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import { defineConfig } from "@datrix/core"
|
|
41
|
+
import { PostgresAdapter } from "@datrix/adapter-postgres"
|
|
42
|
+
import { postSchema, userSchema } from "./schemas"
|
|
43
|
+
|
|
44
|
+
const getDatrix = defineConfig(() => ({
|
|
45
|
+
adapter: new PostgresAdapter({
|
|
46
|
+
host: "localhost",
|
|
47
|
+
port: 5432,
|
|
48
|
+
database: "mydb",
|
|
49
|
+
user: "postgres",
|
|
50
|
+
password: process.env.DB_PASSWORD,
|
|
51
|
+
}),
|
|
52
|
+
schemas: [userSchema, postSchema],
|
|
53
|
+
plugins: [],
|
|
54
|
+
migration: {
|
|
55
|
+
auto: false,
|
|
56
|
+
directory: "./migrations",
|
|
57
|
+
},
|
|
58
|
+
dev: {
|
|
59
|
+
logging: false,
|
|
60
|
+
},
|
|
61
|
+
}))
|
|
62
|
+
|
|
63
|
+
export default getDatrix
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## CRUD
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
const datrix = await getDatrix()
|
|
70
|
+
|
|
71
|
+
// Read
|
|
72
|
+
const posts = await datrix.findMany("post", { where: { status: "published" }, limit: 10 })
|
|
73
|
+
const post = await datrix.findOne("post", { slug: "hello-world" })
|
|
74
|
+
const byId = await datrix.findById("post", 1)
|
|
75
|
+
const total = await datrix.count("post", { status: "draft" })
|
|
76
|
+
|
|
77
|
+
// Write
|
|
78
|
+
const created = await datrix.create("post", { title: "Hello", status: "draft", author: 1 })
|
|
79
|
+
const updated = await datrix.update("post", created.id, { status: "published" })
|
|
80
|
+
const deleted = await datrix.delete("post", created.id)
|
|
81
|
+
|
|
82
|
+
// Bulk
|
|
83
|
+
const many = await datrix.createMany("post", [{ title: "A" }, { title: "B" }])
|
|
84
|
+
const updated2 = await datrix.updateMany("post", { status: "draft" }, { status: "archived" })
|
|
85
|
+
const deleted2 = await datrix.deleteMany("post", { status: "archived" })
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Every record automatically includes `id`, `createdAt`, and `updatedAt` — these cannot be defined manually.
|
|
89
|
+
|
|
90
|
+
### raw
|
|
91
|
+
|
|
92
|
+
Use `datrix.raw` to bypass plugin hooks (`onBeforeQuery` / `onAfterQuery`). Method signatures are identical — the only difference is that schema lifecycle hooks and plugin hooks do not fire.
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
await datrix.raw.create("post", { title: "Silent insert" })
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Field types
|
|
99
|
+
|
|
100
|
+
| Type | Key options |
|
|
101
|
+
| ---------- | --------------------------------------------------------------------- |
|
|
102
|
+
| `string` | `required`, `minLength`, `maxLength`, `unique`, `pattern`, `default`, `validator` |
|
|
103
|
+
| `number` | `required`, `min`, `max`, `integer`, `unique`, `default`, `validator` |
|
|
104
|
+
| `boolean` | `required`, `default` |
|
|
105
|
+
| `date` | `required`, `min`, `max`, `default` |
|
|
106
|
+
| `enum` | `values` (required), `default` |
|
|
107
|
+
| `array` | `items` (field def), `minItems`, `maxItems`, `unique` |
|
|
108
|
+
| `json` | `required`, `default` |
|
|
109
|
+
| `file` | `allowedTypes`, `maxSize`, `multiple` |
|
|
110
|
+
| `relation` | `kind`, `model`, `foreignKey`, `through`, `onDelete`, `onUpdate` |
|
|
111
|
+
|
|
112
|
+
## Relations
|
|
113
|
+
|
|
114
|
+
Datrix manages foreign keys and junction tables automatically — you never define them manually.
|
|
115
|
+
|
|
116
|
+
| Kind | FK location | Use when |
|
|
117
|
+
| ------------- | ----------------- | ------------------------------------ |
|
|
118
|
+
| `belongsTo` | Current schema | This record owns the FK (N:1) |
|
|
119
|
+
| `hasOne` | Target schema | Target owns the FK (1:1) |
|
|
120
|
+
| `hasMany` | Target schema | Target owns the FK (1:N) |
|
|
121
|
+
| `manyToMany` | Junction table | Junction auto-created unless `through` is set |
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
// belongsTo — adds `authorId` to posts table
|
|
125
|
+
author: { type: "relation", kind: "belongsTo", model: "user", onDelete: "restrict" }
|
|
126
|
+
|
|
127
|
+
// hasMany — adds `postId` to comments table
|
|
128
|
+
comments: { type: "relation", kind: "hasMany", model: "comment" }
|
|
129
|
+
|
|
130
|
+
// manyToMany — creates post_tag junction table
|
|
131
|
+
tags: { type: "relation", kind: "manyToMany", model: "tag" }
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Querying
|
|
135
|
+
|
|
136
|
+
### Filtering
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
// Comparison operators
|
|
140
|
+
await datrix.findMany("user", {
|
|
141
|
+
where: {
|
|
142
|
+
age: { $gte: 18, $lte: 65 },
|
|
143
|
+
email: { $like: "%@example.com" },
|
|
144
|
+
role: { $in: ["admin", "editor"] },
|
|
145
|
+
},
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
// Logical operators
|
|
149
|
+
await datrix.findMany("post", {
|
|
150
|
+
where: { $or: [{ status: "published" }, { featured: true }] },
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
// Nested relation filter
|
|
154
|
+
await datrix.findMany("post", {
|
|
155
|
+
where: { author: { verified: true } },
|
|
156
|
+
})
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
All comparison operators: `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$like`, `$ilike`, `$startsWith`, `$endsWith`, `$contains`, `$null`, `$notNull`, `$exists`.
|
|
160
|
+
|
|
161
|
+
### Populate
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
// All relations
|
|
165
|
+
await datrix.findMany("post", { populate: "*" })
|
|
166
|
+
|
|
167
|
+
// Specific relations with options
|
|
168
|
+
await datrix.findMany("post", {
|
|
169
|
+
populate: {
|
|
170
|
+
author: { select: ["id", "name"] },
|
|
171
|
+
comments: { where: { approved: true }, limit: 5, orderBy: { createdAt: "desc" } },
|
|
172
|
+
},
|
|
173
|
+
})
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Sorting and pagination
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
await datrix.findMany("post", {
|
|
180
|
+
orderBy: [{ field: "createdAt", direction: "desc", nulls: "last" }],
|
|
181
|
+
limit: 20,
|
|
182
|
+
offset: 40,
|
|
183
|
+
})
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Lifecycle hooks
|
|
187
|
+
|
|
188
|
+
Hooks run on every non-raw query. They fire after plugin hooks. Every `before` hook must return the (optionally modified) value.
|
|
189
|
+
|
|
190
|
+
`ctx.metadata` is a mutable object shared between the `before` and `after` hook of the same operation.
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
defineSchema({
|
|
194
|
+
name: "post",
|
|
195
|
+
fields: { ... },
|
|
196
|
+
hooks: {
|
|
197
|
+
beforeCreate: async (data, ctx) => {
|
|
198
|
+
return { ...data, slug: data.title?.toLowerCase().replace(/ /g, "-") }
|
|
199
|
+
},
|
|
200
|
+
afterCreate: async (record, ctx) => {
|
|
201
|
+
return record
|
|
202
|
+
},
|
|
203
|
+
beforeFind: async (query, ctx) => {
|
|
204
|
+
return { ...query, where: { ...query.where, status: "published" } }
|
|
205
|
+
},
|
|
206
|
+
afterFind: async (results, ctx) => {
|
|
207
|
+
return results
|
|
208
|
+
},
|
|
209
|
+
beforeDelete: async (id, ctx) => id,
|
|
210
|
+
afterDelete: async (id, ctx) => {},
|
|
211
|
+
},
|
|
212
|
+
})
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Permissions
|
|
216
|
+
|
|
217
|
+
Schema and field permissions are defined in the schema and enforced by `@datrix/api`. The core package carries the config — enforcement is in the API layer.
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
defineSchema({
|
|
221
|
+
name: "post",
|
|
222
|
+
permission: {
|
|
223
|
+
create: ["admin", "editor"],
|
|
224
|
+
read: true,
|
|
225
|
+
update: (ctx) => ctx.user?.id === ctx.record?.authorId,
|
|
226
|
+
delete: ["admin"],
|
|
227
|
+
},
|
|
228
|
+
fields: {
|
|
229
|
+
email: {
|
|
230
|
+
type: "string",
|
|
231
|
+
permission: {
|
|
232
|
+
read: ["admin"], // stripped from response for other roles
|
|
233
|
+
write: ["admin"], // 403 for other roles on create/update
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
})
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Migration
|
|
241
|
+
|
|
242
|
+
Migrations are managed through the Datrix CLI. The core package exposes `beginMigrate()` which the CLI uses internally.
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
datrix migrate # diff schemas against DB, prompt for ambiguous changes, apply
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Direct API use is possible but not the intended workflow:
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
const session = await datrix.beginMigrate()
|
|
252
|
+
|
|
253
|
+
if (session.hasAmbiguous) {
|
|
254
|
+
// Resolve rename vs. drop+add for each ambiguous change
|
|
255
|
+
session.resolveAmbiguous("user.name->lastname", "rename")
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
await session.apply()
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## Architecture
|
|
262
|
+
|
|
263
|
+
```text
|
|
264
|
+
src/
|
|
265
|
+
├── index.ts # Public exports (defineSchema, defineConfig)
|
|
266
|
+
├── datrix.ts # Datrix class — instance factory, CRUD dispatcher
|
|
267
|
+
├── schema-registry.ts # SchemaRegistry — schema storage and lookup
|
|
268
|
+
├── initializer.ts # Startup sequence — schema finalization, plugin init
|
|
269
|
+
├── mixins/
|
|
270
|
+
│ └── crud.ts # CRUD method implementations
|
|
271
|
+
├── query/
|
|
272
|
+
│ ├── builder.ts # QueryBuilder — constructs QueryObject from options
|
|
273
|
+
│ └── executor.ts # QueryExecutor — routes QueryObject to the adapter
|
|
274
|
+
├── validation/
|
|
275
|
+
│ └── validator.ts # Field-level validation before writes
|
|
276
|
+
├── migration/
|
|
277
|
+
│ ├── migrator.ts # MigrationSession — diff and apply logic
|
|
278
|
+
│ └── planner.ts # Change detection and plan generation
|
|
279
|
+
└── plugin/
|
|
280
|
+
└── plugin.ts # BasePlugin — base class for all Datrix plugins
|
|
281
|
+
```
|