@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 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
+ ```