@kavo/mikroorm 0.6.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/README.md ADDED
@@ -0,0 +1,132 @@
1
+ # @kavo/mikroorm
2
+
3
+ MikroORM adapter for Kavo: implements `RepositoryAdapter`
4
+ (`EntityReader` + `EntityWriter`) from `@kavo/core` over a MikroORM
5
+ `EntityManager`. `TransactionManager` is not implemented — see the
6
+ `@remarks` on that interface in `@kavo/core`.
7
+
8
+ **May depend on:** `@kavo/core`, `@mikro-orm/core` (peer). **Never on:**
9
+ `@kavo/nest` or any framework.
10
+
11
+ Fully implemented: CRUD, filtering/sorting/pagination (including across
12
+ relation paths), soft delete/restore/purge, and nested relation includes
13
+ (via `populate`). MikroORM declares no delete-marker column the way
14
+ TypeORM's `@DeleteDateColumn` does, so the seam reports none — but read the
15
+ soft-delete note under "Known limitations" before assuming that means soft
16
+ delete is off until you configure it.
17
+
18
+ ## Usage
19
+
20
+ Like `@kavo/typeorm` and unlike `@kavo/prisma`, there is nothing to declare
21
+ twice: a MikroORM entity is a real runtime class carrying its own metadata,
22
+ so the class you pass to `createCrud` _is_ the identity core needs — no
23
+ marker classes, no `entities` list beyond the one MikroORM already has.
24
+
25
+ ```ts
26
+ import { Collection, Entity, ManyToOne, OneToMany, PrimaryKey, Property, MikroORM } from "@mikro-orm/core";
27
+ import { defineConfig } from "@mikro-orm/postgresql";
28
+ import { createMikroOrmKavo } from "@kavo/mikroorm";
29
+
30
+ @Entity()
31
+ class Author {
32
+ @PrimaryKey({ type: "number" })
33
+ id!: number;
34
+
35
+ @Property({ type: "string" })
36
+ name!: string;
37
+
38
+ @OneToMany(() => Book, (book) => book.author)
39
+ books = new Collection<Book>(this);
40
+ }
41
+
42
+ const orm = await MikroORM.init(defineConfig({ dbName: "app", entities: [Author, Book] }));
43
+ const kavo = createMikroOrmKavo(orm, { caseInsensitiveFilters: true });
44
+
45
+ const authors = kavo.createCrud(Author);
46
+ ```
47
+
48
+ `createInfrastructure`/`createMikroOrmKavo` take the `MikroORM` instance
49
+ itself rather than an `EntityManager`: every adapter operation calls
50
+ `orm.em.fork()` to get its own manager, which is what keeps one request's
51
+ identity map out of the next one's.
52
+
53
+ ## `caseInsensitiveFilters`
54
+
55
+ `ILIKE` maps to MikroORM's `$ilike`, which **only PostgreSQL supports** —
56
+ SQLite, MySQL, and MongoDB pass the token through to the driver and fail
57
+ with a syntax error. MikroORM's `Platform` exposes nothing to detect this
58
+ from, so it is a declared setting, defaulting to `false` (the value that
59
+ works everywhere). Turn it on for PostgreSQL.
60
+
61
+ With it off, `ILIKE` translates exactly like `LIKE`. On SQLite that is not
62
+ even a loss — SQLite's own `LIKE` is already ASCII case-insensitive.
63
+
64
+ ## Known limitations
65
+
66
+ - **`LIKE` escapes are driver-dependent.** MikroORM has no way to attach an
67
+ `ESCAPE` clause to a `LIKE`, so the query grammar's `\` escape for a
68
+ literal `%`/`_` is honored only by drivers that default to backslash
69
+ (PostgreSQL, MySQL). On SQLite, which has no default escape character,
70
+ `filter[name][like]=100\%` matches the literal text `100\` followed by
71
+ anything, rather than the string `100%`.
72
+ - **Soft-deleted related rows are pruned in memory.** MikroORM's
73
+ `populateWhere` cannot express per-level scoping: nesting a child condition
74
+ makes it a predicate on the _parent_, so a parent with no surviving
75
+ children disappears instead of coming back empty. The adapter populates
76
+ everything and prunes the loaded tree, which is correct at any depth but
77
+ does fetch soft-deleted related rows before discarding them. Soft-deleted
78
+ _roots_ are still excluded in SQL.
79
+ - **`IncludeNode.strategy` is ignored.** MikroORM resolves `populate` with
80
+ its own queries and applies `limit`/`offset` to the root either way, so
81
+ the join/batch distinction has nothing to control here — and MikroORM's
82
+ `strategy` option is per-query rather than per-relation, so it could not
83
+ express a mixed include tree anyway. Same posture as `@kavo/prisma`.
84
+ - **MikroORM's own property options win at the boundary.** Rows are
85
+ converted with `wrap(entity).toObject()`, so a custom `serializer` runs
86
+ before core ever sees the row.
87
+ - **The soft-delete marker is writable, and it is on by default.** Nothing
88
+ in a MikroORM entity declares a delete column, so the adapter cannot mark
89
+ it generated — but `softDelete` defaults to
90
+ `{ field: "deletedAt", strategy: "auto" }` and core matches that _name_
91
+ against your columns, so any entity with a `deletedAt` property is
92
+ soft-deletable with no config at all. The marker then sits in the derived
93
+ writable projection: a plain `PATCH` of it soft-deletes a row, bypassing
94
+ `deleteOne`'s already-deleted check and even
95
+ `operations: { deleteOne: false }`. It cannot _revive_ one — writes are
96
+ scoped to the live set, so a soft-deleted row 404s on `PUT`/`PATCH`. This
97
+ is the same hole `@kavo/prisma` and `@kavo/mongoose` have (only
98
+ `@kavo/typeorm` escapes it, because `@DeleteDateColumn` is detectable and
99
+ therefore markable), and the fix belongs in core. Until then, register an
100
+ explicit `update`/`patch` DTO that omits the marker.
101
+ - **A non-auto-increment primary key is client-writable.**
102
+ `@PrimaryKey() id: string = v4()` carries none of MikroORM's generated
103
+ flags, so a `PATCH` can rewrite a row's identity. A numeric `@PrimaryKey()`
104
+ is auto-increment and safe. Name the write DTOs explicitly for any entity
105
+ with a caller-assigned key.
106
+ - **`hidden` and `lazy` properties are dropped from Kavo entirely.** Not
107
+ merely hidden from responses — excluding them from the metadata seam is
108
+ what keeps them off the default filter/sort allowlists, where an invisible
109
+ but filterable column is a blind extraction oracle. The trade is the one
110
+ `@kavo/mongoose` makes for `select: false`: Kavo does not manage such a
111
+ property at all, so write it through a custom operation or the ORM.
112
+ - **A MikroORM `@Filter` is applied on top of Kavo's scoping.** Kavo owns
113
+ soft-delete scoping through `softDelete.field`; a default-on MikroORM
114
+ soft-delete filter would AND a second predicate onto every query and
115
+ quietly defeat `withDeleted`. Use one or the other, not both.
116
+ - **No transactions.** The `TransactionManager` seam is unbuilt across every
117
+ Kavo adapter today.
118
+ - **Composite primary keys are refused.** `buildEntityMetadata` raises
119
+ `KAVO_CONFIG_INVALID` for an entity with more than one `@PrimaryKey`.
120
+
121
+ ## Soft delete and unique indexes
122
+
123
+ A soft-deleted row still occupies its unique indexes, so re-creating "the
124
+ same" row after a soft delete raises a 409 conflict — the honest answer,
125
+ since the value _is_ taken. The fix is a partial unique index scoped to
126
+ live rows:
127
+
128
+ ```sql
129
+ CREATE UNIQUE INDEX author_email_live ON author (email) WHERE deleted_at IS NULL;
130
+ ```
131
+
132
+ Full design notes: [`docs/internals/architecture/17-mikroorm-adapter.md`](../../../docs/internals/architecture/17-mikroorm-adapter.md).
@@ -0,0 +1,32 @@
1
+ import type { ErrorContext } from "@kavo/core";
2
+ import { KavoException } from "@kavo/core";
3
+ /**
4
+ * The error-mapping table: driver error → Kavo exception.
5
+ *
6
+ * | MikroORM condition | Exception |
7
+ * | ------------------------------------------- | ---------------------------------- |
8
+ * | `UniqueConstraintViolationException` | ConflictException |
9
+ * | `ForeignKeyConstraintViolationException` | ConflictException |
10
+ * | `DeadlockException` / `LockWaitTimeoutException` | TransactionException (retryable) |
11
+ * | anything else | PersistenceException with `cause` |
12
+ *
13
+ * The same four rows `@kavo/typeorm`'s table has, reached differently:
14
+ * MikroORM normalizes each driver's native error into its own exception
15
+ * hierarchy before it surfaces, so this matches on those classes rather than
16
+ * on Postgres SQLSTATEs, MySQL errnos, and SQLite extended codes the way the
17
+ * TypeORM adapter must. Every driver MikroORM supports is covered by that
18
+ * normalization, and anything it does not recognize falls through to
19
+ * `PersistenceException` — the original error always travels as `cause`,
20
+ * never swallowed.
21
+ *
22
+ * **Soft delete and unique indexes.** A soft-deleted row still occupies its
23
+ * unique indexes, so re-creating "the same" row after a soft delete raises a
24
+ * unique violation — mapped here to a 409 like any other conflict, which is
25
+ * the honest answer: the value *is* taken. Kavo never rewrites indexes; the
26
+ * standard fix is a partial/filtered unique index on the live rows only,
27
+ * e.g. in Postgres
28
+ * `CREATE UNIQUE INDEX … ON author (email) WHERE deleted_at IS NULL`
29
+ * (SQLite supports the same form; MySQL needs a generated column).
30
+ */
31
+ export declare function mapDriverError(error: unknown, context: ErrorContext): KavoException;
32
+ //# sourceMappingURL=error-mapping.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-mapping.d.ts","sourceRoot":"","sources":["../src/error-mapping.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAqB,aAAa,EAA8C,MAAM,YAAY,CAAC;AAQ1G;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,YAAY,GAAG,aAAa,CAsBnF"}
@@ -0,0 +1,51 @@
1
+ import { ConflictException, KavoException, PersistenceException, TransactionException } from "@kavo/core";
2
+ import { DeadlockException, ForeignKeyConstraintViolationException, LockWaitTimeoutException, UniqueConstraintViolationException, } from "@mikro-orm/core";
3
+ /**
4
+ * The error-mapping table: driver error → Kavo exception.
5
+ *
6
+ * | MikroORM condition | Exception |
7
+ * | ------------------------------------------- | ---------------------------------- |
8
+ * | `UniqueConstraintViolationException` | ConflictException |
9
+ * | `ForeignKeyConstraintViolationException` | ConflictException |
10
+ * | `DeadlockException` / `LockWaitTimeoutException` | TransactionException (retryable) |
11
+ * | anything else | PersistenceException with `cause` |
12
+ *
13
+ * The same four rows `@kavo/typeorm`'s table has, reached differently:
14
+ * MikroORM normalizes each driver's native error into its own exception
15
+ * hierarchy before it surfaces, so this matches on those classes rather than
16
+ * on Postgres SQLSTATEs, MySQL errnos, and SQLite extended codes the way the
17
+ * TypeORM adapter must. Every driver MikroORM supports is covered by that
18
+ * normalization, and anything it does not recognize falls through to
19
+ * `PersistenceException` — the original error always travels as `cause`,
20
+ * never swallowed.
21
+ *
22
+ * **Soft delete and unique indexes.** A soft-deleted row still occupies its
23
+ * unique indexes, so re-creating "the same" row after a soft delete raises a
24
+ * unique violation — mapped here to a 409 like any other conflict, which is
25
+ * the honest answer: the value *is* taken. Kavo never rewrites indexes; the
26
+ * standard fix is a partial/filtered unique index on the live rows only,
27
+ * e.g. in Postgres
28
+ * `CREATE UNIQUE INDEX … ON author (email) WHERE deleted_at IS NULL`
29
+ * (SQLite supports the same form; MySQL needs a generated column).
30
+ */
31
+ export function mapDriverError(error, context) {
32
+ if (error instanceof KavoException)
33
+ return error;
34
+ const entity = context.entityName ?? "entity";
35
+ if (error instanceof UniqueConstraintViolationException || error instanceof ForeignKeyConstraintViolationException) {
36
+ return new ConflictException({
37
+ messageParams: { entity },
38
+ context,
39
+ cause: error,
40
+ });
41
+ }
42
+ if (error instanceof DeadlockException || error instanceof LockWaitTimeoutException) {
43
+ return new TransactionException({ retryable: true, context, cause: error });
44
+ }
45
+ return new PersistenceException({
46
+ messageParams: { operation: context.operation ?? "unknown" },
47
+ context,
48
+ cause: error,
49
+ });
50
+ }
51
+ //# sourceMappingURL=error-mapping.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-mapping.js","sourceRoot":"","sources":["../src/error-mapping.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAC1G,OAAO,EACL,iBAAiB,EACjB,sCAAsC,EACtC,wBAAwB,EACxB,kCAAkC,GACnC,MAAM,iBAAiB,CAAC;AAEzB;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,UAAU,cAAc,CAAC,KAAc,EAAE,OAAqB;IAClE,IAAI,KAAK,YAAY,aAAa;QAAE,OAAO,KAAK,CAAC;IAEjD,MAAM,MAAM,GAAG,OAAO,CAAC,UAAU,IAAI,QAAQ,CAAC;IAE9C,IAAI,KAAK,YAAY,kCAAkC,IAAI,KAAK,YAAY,sCAAsC,EAAE,CAAC;QACnH,OAAO,IAAI,iBAAiB,CAAC;YAC3B,aAAa,EAAE,EAAE,MAAM,EAAE;YACzB,OAAO;YACP,KAAK,EAAE,KAAK;SACb,CAAC,CAAC;IACL,CAAC;IAED,IAAI,KAAK,YAAY,iBAAiB,IAAI,KAAK,YAAY,wBAAwB,EAAE,CAAC;QACpF,OAAO,IAAI,oBAAoB,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;IAC9E,CAAC;IAED,OAAO,IAAI,oBAAoB,CAAC;QAC9B,aAAa,EAAE,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,IAAI,SAAS,EAAE;QAC5D,OAAO;QACP,KAAK,EAAE,KAAK;KACb,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,49 @@
1
+ import type { Filter } from "@kavo/core";
2
+ /**
3
+ * A MikroORM `FilterQuery`, built structurally as a plain object. MikroORM's
4
+ * own `FilterQuery<T>` is entity-generic and deliberately not used here: the
5
+ * translator works from core's AST, which is not tied to a MikroORM entity
6
+ * type, and a plain object is what lets this be unit-tested without a
7
+ * database.
8
+ */
9
+ export type MikroWhere = Record<string, unknown>;
10
+ export interface FilterTranslatorOptions {
11
+ /**
12
+ * The entity's primary-key property. Used only to spell a contradiction
13
+ * for the degenerate empty-group cases — see {@link matchesNothing}.
14
+ */
15
+ readonly idField: string;
16
+ /**
17
+ * Whether the target driver supports MikroORM's `$ilike` operator.
18
+ * PostgreSQL does; SQLite, MySQL, and MongoDB do not — MikroORM passes
19
+ * `ilike` straight through to SQL, so on those drivers it is a syntax
20
+ * error rather than a degraded match. A MikroORM `Platform` exposes
21
+ * nothing to detect this from, so it is a caller-declared setting (see
22
+ * `MikroOrmInfrastructureOptions.caseInsensitiveFilters`), the same
23
+ * posture `@kavo/prisma` takes for `mode: "insensitive"`.
24
+ *
25
+ * Unlike `@kavo/prisma` this defaults to **`false`**, because the two
26
+ * failure modes are not symmetric: declaring it off on PostgreSQL costs
27
+ * case-insensitivity, while leaving it on anywhere else makes every
28
+ * `ILIKE` query throw. On SQLite the default is not even a loss —
29
+ * SQLite's own `LIKE` is already ASCII case-insensitive.
30
+ */
31
+ readonly caseInsensitiveFilters: boolean;
32
+ }
33
+ /**
34
+ * Filter AST → MikroORM `FilterQuery`.
35
+ *
36
+ * Like `@kavo/prisma`'s translator and unlike `@kavo/typeorm`'s, this needs
37
+ * no query-builder state — no join aliases, no parameter numbering. MikroORM
38
+ * nests relation paths natively (`{ author: { name: { $eq: "Ada" } } }`) and
39
+ * adds the join itself, so a relation-path field nests the same translation
40
+ * one level deeper instead of registering a join.
41
+ *
42
+ * Every comparison is wrapped in an explicit operator (`{ $eq: v }`, never
43
+ * the bare `{ field: v }` shorthand). Core coerces filter values to scalars
44
+ * upstream, so this is defence in depth rather than the only guard — but an
45
+ * object arriving through the shorthand would be spliced in as *operators*,
46
+ * and the boundary's job is to be safe on its own terms.
47
+ */
48
+ export declare function translateFilter<Entity>(filter: Filter<Entity>, options: FilterTranslatorOptions): MikroWhere | undefined;
49
+ //# sourceMappingURL=filter-translator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filter-translator.d.ts","sourceRoot":"","sources":["../src/filter-translator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAmD,MAAM,YAAY,CAAC;AAG1F;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEjD,MAAM,WAAW,uBAAuB;IACtC;;;OAGG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,sBAAsB,EAAE,OAAO,CAAC;CAC1C;AAgBD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,eAAe,CAAC,MAAM,EACpC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,EACtB,OAAO,EAAE,uBAAuB,GAC/B,UAAU,GAAG,SAAS,CAIxB"}
@@ -0,0 +1,109 @@
1
+ import { assertNever } from "@kavo/core";
2
+ /**
3
+ * A predicate that matches no row.
4
+ *
5
+ * MikroORM rejects an empty `$and`/`$or` outright, and `$not: {}` negates
6
+ * *no condition at all*, which it renders as a match-everything rather than
7
+ * a match-nothing — the opposite of what these cases need. An empty `$in`
8
+ * is the one spelling MikroORM turns into a genuine contradiction on every
9
+ * driver, which is why this is expressed against the primary key rather
10
+ * than with a bare connective.
11
+ */
12
+ function matchesNothing(idField) {
13
+ return { [idField]: { $in: [] } };
14
+ }
15
+ /**
16
+ * Filter AST → MikroORM `FilterQuery`.
17
+ *
18
+ * Like `@kavo/prisma`'s translator and unlike `@kavo/typeorm`'s, this needs
19
+ * no query-builder state — no join aliases, no parameter numbering. MikroORM
20
+ * nests relation paths natively (`{ author: { name: { $eq: "Ada" } } }`) and
21
+ * adds the join itself, so a relation-path field nests the same translation
22
+ * one level deeper instead of registering a join.
23
+ *
24
+ * Every comparison is wrapped in an explicit operator (`{ $eq: v }`, never
25
+ * the bare `{ field: v }` shorthand). Core coerces filter values to scalars
26
+ * upstream, so this is defence in depth rather than the only guard — but an
27
+ * object arriving through the shorthand would be spliced in as *operators*,
28
+ * and the boundary's job is to be safe on its own terms.
29
+ */
30
+ export function translateFilter(filter, options) {
31
+ const root = filter.root;
32
+ if (root === null)
33
+ return undefined;
34
+ return translateExpression(root, options);
35
+ }
36
+ function translateExpression(expression, options) {
37
+ if (expression.kind === "condition") {
38
+ return translateCondition(expression, options);
39
+ }
40
+ const children = expression.children.map((child) => translateExpression(child, options));
41
+ if (expression.operator === "NOT") {
42
+ // Core's parser gives a NOT group exactly one child. With no child there
43
+ // is nothing to negate, and "not (anything)" matches nothing.
44
+ return children.length === 0 ? matchesNothing(options.idField) : { $not: children[0] };
45
+ }
46
+ // The parser enforces arity, so the empty cases only guard hand-built ASTs
47
+ // passed programmatically — but MikroORM rejects an empty `$and`/`$or`, so
48
+ // the identity for each connective is spelled explicitly.
49
+ if (children.length === 0) {
50
+ return expression.operator === "OR" ? matchesNothing(options.idField) : {};
51
+ }
52
+ return expression.operator === "OR" ? { $or: children } : { $and: children };
53
+ }
54
+ /** Nest `condition.field` (`"author.name"`) into MikroORM's native relation-path shape. */
55
+ function nest(field, leaf) {
56
+ return field.split(".").reduceRight((inner, segment) => ({ [segment]: inner }), leaf);
57
+ }
58
+ function translateCondition(condition, options) {
59
+ const field = condition.field;
60
+ const value = condition.value;
61
+ switch (condition.operator) {
62
+ case "EQ":
63
+ return nest(field, { $eq: value });
64
+ case "NE":
65
+ // `$ne: null` is MikroORM's `IS NOT NULL`, so a null value needs no
66
+ // separate branch the way the SQL-string translator's does.
67
+ return nest(field, { $ne: value });
68
+ case "GT":
69
+ return nest(field, { $gt: value });
70
+ case "GTE":
71
+ return nest(field, { $gte: value });
72
+ case "LT":
73
+ return nest(field, { $lt: value });
74
+ case "LTE":
75
+ return nest(field, { $lte: value });
76
+ case "IN":
77
+ // MikroORM renders an empty `$in` as a contradiction rather than
78
+ // emitting invalid `IN ()`, and an empty `$nin` as a tautology — the
79
+ // same answers `@kavo/typeorm` has to spell out by hand because it
80
+ // writes the SQL itself.
81
+ return nest(field, { $in: value });
82
+ case "NOT_IN":
83
+ return nest(field, { $nin: value });
84
+ case "LIKE":
85
+ // The pattern reaches SQL `LIKE` verbatim. MikroORM has no way to
86
+ // attach an `ESCAPE` clause, so the grammar's `\` escape for a literal
87
+ // `%`/`_` is honored only by drivers that default to backslash
88
+ // (PostgreSQL, MySQL) — not by SQLite, which has no default escape
89
+ // character. See doc 17, "Adapter-specific caveats".
90
+ return nest(field, { $like: value });
91
+ case "ILIKE":
92
+ return nest(field, options.caseInsensitiveFilters ? { $ilike: value } : { $like: value });
93
+ case "BETWEEN": {
94
+ const [low, high] = value;
95
+ return nest(field, { $gte: low, $lte: high });
96
+ }
97
+ case "IS_NULL":
98
+ return nest(field, { $eq: null });
99
+ case "IS_NOT_NULL":
100
+ return nest(field, { $ne: null });
101
+ default:
102
+ // Every AST operator must translate to a predicate. Falling through
103
+ // silently would drop the condition and widen the result set, so the
104
+ // union is proven total here at build time — the same guarantee the
105
+ // other adapters' translators give.
106
+ return assertNever(condition.operator, "filter operator");
107
+ }
108
+ }
109
+ //# sourceMappingURL=filter-translator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filter-translator.js","sourceRoot":"","sources":["../src/filter-translator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @kavo/mikroorm — MikroORM adapter for Kavo.
3
+ *
4
+ * Implements `@kavo/core`'s `RepositoryAdapter` over a MikroORM
5
+ * `EntityManager` and feeds core's entity-metadata seam from MikroORM's own
6
+ * `MetadataStorage`. `@mikro-orm/core` is a peerDependency; `@kavo/core`
7
+ * never imports it.
8
+ */
9
+ export { MikroOrmRepositoryAdapter } from "./mikro-orm-repository-adapter.js";
10
+ export { translateFilter, type FilterTranslatorOptions, type MikroWhere } from "./filter-translator.js";
11
+ export { buildEntityMetadata } from "./metadata.js";
12
+ export { mapDriverError } from "./error-mapping.js";
13
+ export { toPlain, toPlainAll } from "./plain-entity.js";
14
+ export { createInfrastructure, createMikroOrmKavo, type MikroOrmInfrastructureOptions } from "./infrastructure.js";
15
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,yBAAyB,EAAE,MAAM,mCAAmC,CAAC;AAC9E,OAAO,EAAE,eAAe,EAAE,KAAK,uBAAuB,EAAE,KAAK,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACxG,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,KAAK,6BAA6B,EAAE,MAAM,qBAAqB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @kavo/mikroorm — MikroORM adapter for Kavo.
3
+ *
4
+ * Implements `@kavo/core`'s `RepositoryAdapter` over a MikroORM
5
+ * `EntityManager` and feeds core's entity-metadata seam from MikroORM's own
6
+ * `MetadataStorage`. `@mikro-orm/core` is a peerDependency; `@kavo/core`
7
+ * never imports it.
8
+ */
9
+ export { MikroOrmRepositoryAdapter } from "./mikro-orm-repository-adapter.js";
10
+ export { translateFilter } from "./filter-translator.js";
11
+ export { buildEntityMetadata } from "./metadata.js";
12
+ export { mapDriverError } from "./error-mapping.js";
13
+ export { toPlain, toPlainAll } from "./plain-entity.js";
14
+ export { createInfrastructure, createMikroOrmKavo } from "./infrastructure.js";
15
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,yBAAyB,EAAE,MAAM,mCAAmC,CAAC;AAC9E,OAAO,EAAE,eAAe,EAAiD,MAAM,wBAAwB,CAAC;AACxG,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,oBAAoB,EAAE,kBAAkB,EAAsC,MAAM,qBAAqB,CAAC"}
@@ -0,0 +1,35 @@
1
+ import type { KavoInfrastructure, KavoInstance, KavoOptions } from "@kavo/core";
2
+ import type { MikroORM } from "@mikro-orm/core";
3
+ /**
4
+ * What MikroORM needs beyond the ORM instance. Nothing is required — a
5
+ * MikroORM entity is a real runtime class carrying its own metadata, so the
6
+ * ORM instance alone supplies both halves of core's infrastructure seam,
7
+ * exactly as a TypeORM `DataSource` does and unlike `@kavo/prisma`, which
8
+ * needs caller-declared marker classes (ADR-0017).
9
+ */
10
+ export interface MikroOrmInfrastructureOptions {
11
+ /**
12
+ * Whether the driver supports MikroORM's `$ilike` operator — PostgreSQL
13
+ * does; SQLite, MySQL, and MongoDB reject it as a syntax error. Defaults
14
+ * to `false`, the setting that works on every driver; turn it on for
15
+ * PostgreSQL to make `ILIKE` genuinely case-insensitive. See
16
+ * `FilterTranslatorOptions`.
17
+ */
18
+ readonly caseInsensitiveFilters?: boolean;
19
+ }
20
+ /**
21
+ * The MikroORM implementation of core's infrastructure seam: metadata and
22
+ * adapters derived from one `MikroORM` instance, cached per entity (metadata
23
+ * derivation and adapter construction are bootstrap work, not per-request
24
+ * work) — same shape as `@kavo/typeorm`'s `createInfrastructure`.
25
+ *
26
+ * The instance is held rather than an `EntityManager`, because every adapter
27
+ * operation forks its own manager from it; see `MikroOrmRepositoryAdapter`.
28
+ */
29
+ export declare function createInfrastructure(orm: MikroORM, options?: MikroOrmInfrastructureOptions): KavoInfrastructure;
30
+ /**
31
+ * Sugar for the common case: a Kavo root instance wired to one MikroORM
32
+ * instance, so `kavo.createCrud(Author)` is genuinely zero-config.
33
+ */
34
+ export declare function createMikroOrmKavo(orm: MikroORM, options?: MikroOrmInfrastructureOptions & Omit<KavoOptions, "infrastructure">): KavoInstance;
35
+ //# sourceMappingURL=infrastructure.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"infrastructure.d.ts","sourceRoot":"","sources":["../src/infrastructure.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAGV,kBAAkB,EAClB,YAAY,EACZ,WAAW,EAEZ,MAAM,YAAY,CAAC;AAEpB,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAIhD;;;;;;GAMG;AACH,MAAM,WAAW,6BAA6B;IAC5C;;;;;;OAMG;IACH,QAAQ,CAAC,sBAAsB,CAAC,EAAE,OAAO,CAAC;CAC3C;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,QAAQ,EAAE,OAAO,GAAE,6BAAkC,GAAG,kBAAkB,CA0BnH;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,QAAQ,EACb,OAAO,GAAE,6BAA6B,GAAG,IAAI,CAAC,WAAW,EAAE,gBAAgB,CAAM,GAChF,YAAY,CAMd"}
@@ -0,0 +1,49 @@
1
+ import { createKavo } from "@kavo/core";
2
+ import { buildEntityMetadata } from "./metadata.js";
3
+ import { MikroOrmRepositoryAdapter } from "./mikro-orm-repository-adapter.js";
4
+ /**
5
+ * The MikroORM implementation of core's infrastructure seam: metadata and
6
+ * adapters derived from one `MikroORM` instance, cached per entity (metadata
7
+ * derivation and adapter construction are bootstrap work, not per-request
8
+ * work) — same shape as `@kavo/typeorm`'s `createInfrastructure`.
9
+ *
10
+ * The instance is held rather than an `EntityManager`, because every adapter
11
+ * operation forks its own manager from it; see `MikroOrmRepositoryAdapter`.
12
+ */
13
+ export function createInfrastructure(orm, options = {}) {
14
+ const metadataCache = new Map();
15
+ const adapterCache = new Map();
16
+ function metadataFor(entity) {
17
+ let metadata = metadataCache.get(entity);
18
+ if (metadata === undefined) {
19
+ metadata = buildEntityMetadata(orm, entity);
20
+ metadataCache.set(entity, metadata);
21
+ }
22
+ return metadata;
23
+ }
24
+ return {
25
+ metadataFor,
26
+ adapterFor(entity) {
27
+ let adapter = adapterCache.get(entity);
28
+ if (adapter === undefined) {
29
+ adapter = new MikroOrmRepositoryAdapter(orm, metadataFor(entity), {
30
+ caseInsensitiveFilters: options.caseInsensitiveFilters ?? false,
31
+ });
32
+ adapterCache.set(entity, adapter);
33
+ }
34
+ return adapter;
35
+ },
36
+ };
37
+ }
38
+ /**
39
+ * Sugar for the common case: a Kavo root instance wired to one MikroORM
40
+ * instance, so `kavo.createCrud(Author)` is genuinely zero-config.
41
+ */
42
+ export function createMikroOrmKavo(orm, options = {}) {
43
+ const { caseInsensitiveFilters, ...kavoOptions } = options;
44
+ return createKavo({
45
+ ...kavoOptions,
46
+ infrastructure: createInfrastructure(orm, { caseInsensitiveFilters }),
47
+ });
48
+ }
49
+ //# sourceMappingURL=infrastructure.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"infrastructure.js","sourceRoot":"","sources":["../src/infrastructure.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAExC,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EAAE,yBAAyB,EAAE,MAAM,mCAAmC,CAAC;AAoB9E;;;;;;;;GAQG;AACH,MAAM,UAAU,oBAAoB,CAAC,GAAa,EAAE,UAAyC,EAAE;IAC7F,MAAM,aAAa,GAAG,IAAI,GAAG,EAA4B,CAAC;IAC1D,MAAM,YAAY,GAAG,IAAI,GAAG,EAA+B,CAAC;IAE5D,SAAS,WAAW,CAAwB,MAAwB;QAClE,IAAI,QAAQ,GAAG,aAAa,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACzC,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC3B,QAAQ,GAAG,mBAAmB,CAAC,GAAG,EAAE,MAAM,CAAmB,CAAC;YAC9D,aAAa,CAAC,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;QACtC,CAAC;QACD,OAAO,QAAkC,CAAC;IAC5C,CAAC;IAED,OAAO;QACL,WAAW;QACX,UAAU,CAAwB,MAAwB;YACxD,IAAI,OAAO,GAAG,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YACvC,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;gBAC1B,OAAO,GAAG,IAAI,yBAAyB,CAAC,GAAG,EAAE,WAAW,CAAC,MAAM,CAAC,EAAE;oBAChE,sBAAsB,EAAE,OAAO,CAAC,sBAAsB,IAAI,KAAK;iBAChE,CAAsB,CAAC;gBACxB,YAAY,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;YACpC,CAAC;YACD,OAAO,OAAoC,CAAC;QAC9C,CAAC;KACF,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAChC,GAAa,EACb,UAA+E,EAAE;IAEjF,MAAM,EAAE,sBAAsB,EAAE,GAAG,WAAW,EAAE,GAAG,OAAO,CAAC;IAC3D,OAAO,UAAU,CAAC;QAChB,GAAG,WAAW;QACd,cAAc,EAAE,oBAAoB,CAAC,GAAG,EAAE,EAAE,sBAAsB,EAAE,CAAC;KACtE,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,15 @@
1
+ import type { ClassRef, EntityMetadata } from "@kavo/core";
2
+ import type { MikroORM } from "@mikro-orm/core";
3
+ /**
4
+ * Build the core `EntityMetadata` for one entity from MikroORM's own
5
+ * `MetadataStorage`: the adapter feeds core's metadata seam; core never sees
6
+ * MikroORM types.
7
+ *
8
+ * Unlike `@kavo/prisma`, no marker class is needed (ADR-0017 exists because
9
+ * Prisma erases its models at compile time) — a MikroORM entity is a real
10
+ * runtime class that already carries its own metadata, so the class the
11
+ * caller passes to `createCrud` *is* the identity, exactly as in
12
+ * `@kavo/typeorm`.
13
+ */
14
+ export declare function buildEntityMetadata<Entity extends object>(orm: MikroORM, entity: ClassRef<Entity>): EntityMetadata<Entity>;
15
+ //# sourceMappingURL=metadata.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAgD,MAAM,YAAY,CAAC;AAEzG,OAAO,KAAK,EAAkB,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAyGhE;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,SAAS,MAAM,EACvD,GAAG,EAAE,QAAQ,EACb,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,GACvB,cAAc,CAAC,MAAM,CAAC,CAmGxB"}