metaobjects 0.9.0__py3-none-any.whl
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.
- metaobjects/__init__.py +75 -0
- metaobjects/agent_context/__init__.py +55 -0
- metaobjects/agent_context/_content/README.md +14 -0
- metaobjects/agent_context/_content/servers/csharp.meta.json +5 -0
- metaobjects/agent_context/_content/servers/java.meta.json +5 -0
- metaobjects/agent_context/_content/servers/kotlin.meta.json +5 -0
- metaobjects/agent_context/_content/servers/python.meta.json +5 -0
- metaobjects/agent_context/_content/servers/typescript.meta.json +5 -0
- metaobjects/agent_context/_content/skills/metaobjects-authoring/SKILL.md +301 -0
- metaobjects/agent_context/_content/skills/metaobjects-codegen/SKILL.md +99 -0
- metaobjects/agent_context/_content/skills/metaobjects-codegen/references/csharp.md +87 -0
- metaobjects/agent_context/_content/skills/metaobjects-codegen/references/java.md +94 -0
- metaobjects/agent_context/_content/skills/metaobjects-codegen/references/kotlin.md +110 -0
- metaobjects/agent_context/_content/skills/metaobjects-codegen/references/typescript.md +135 -0
- metaobjects/agent_context/_content/skills/metaobjects-prompts/SKILL.md +148 -0
- metaobjects/agent_context/_content/skills/metaobjects-prompts/references/csharp.md +110 -0
- metaobjects/agent_context/_content/skills/metaobjects-prompts/references/java.md +108 -0
- metaobjects/agent_context/_content/skills/metaobjects-prompts/references/kotlin.md +130 -0
- metaobjects/agent_context/_content/skills/metaobjects-prompts/references/python.md +116 -0
- metaobjects/agent_context/_content/skills/metaobjects-prompts/references/typescript.md +150 -0
- metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/SKILL.md +130 -0
- metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/java.md +96 -0
- metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/kotlin.md +99 -0
- metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/react.md +86 -0
- metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/tanstack.md +119 -0
- metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/typescript.md +92 -0
- metaobjects/agent_context/_content/skills/metaobjects-verify/SKILL.md +107 -0
- metaobjects/agent_context/_content/skills/metaobjects-verify/references/migration.md +72 -0
- metaobjects/agent_context/_content/templates/always-on.md.mustache +27 -0
- metaobjects/agent_context/assemble.py +133 -0
- metaobjects/agent_context/content_root.py +54 -0
- metaobjects/agent_context/scaffold.py +191 -0
- metaobjects/agent_context/types.py +44 -0
- metaobjects/attr_class_map.py +23 -0
- metaobjects/cli.py +696 -0
- metaobjects/codegen/__init__.py +0 -0
- metaobjects/codegen/config.py +11 -0
- metaobjects/codegen/constants.py +13 -0
- metaobjects/codegen/extract_delegate_emitter.py +384 -0
- metaobjects/codegen/extract_schema_emitter.py +139 -0
- metaobjects/codegen/format.py +31 -0
- metaobjects/codegen/fr010_field_mapping.py +220 -0
- metaobjects/codegen/generator.py +62 -0
- metaobjects/codegen/generator_registry.py +163 -0
- metaobjects/codegen/generators/__init__.py +0 -0
- metaobjects/codegen/generators/entity_model.py +263 -0
- metaobjects/codegen/generators/extractor_generator.py +317 -0
- metaobjects/codegen/generators/filter_allowlist_generator.py +309 -0
- metaobjects/codegen/generators/m2m_codegen.py +192 -0
- metaobjects/codegen/generators/output_parser_generator.py +272 -0
- metaobjects/codegen/generators/output_prompt_generator.py +192 -0
- metaobjects/codegen/generators/payload_vo_generator.py +672 -0
- metaobjects/codegen/generators/render_helper_generator.py +451 -0
- metaobjects/codegen/generators/router_generator.py +635 -0
- metaobjects/codegen/generators/template_generator.py +70 -0
- metaobjects/codegen/generators/tph_plan.py +120 -0
- metaobjects/codegen/generators/trace_helper_generator.py +336 -0
- metaobjects/codegen/instance_artifacts.py +15 -0
- metaobjects/codegen/output_format_spec_emitter.py +79 -0
- metaobjects/codegen/overwrite_policy.py +27 -0
- metaobjects/codegen/runner.py +110 -0
- metaobjects/codegen/runtime/__init__.py +6 -0
- metaobjects/codegen/runtime/filter_parser.py +193 -0
- metaobjects/codegen/type_map.py +84 -0
- metaobjects/core_types.py +809 -0
- metaobjects/datatype.py +19 -0
- metaobjects/documentation/__init__.py +28 -0
- metaobjects/documentation/doc_constants.py +20 -0
- metaobjects/documentation/doc_provider.py +20 -0
- metaobjects/documentation/doc_schema.py +24 -0
- metaobjects/errors.py +124 -0
- metaobjects/loader/__init__.py +0 -0
- metaobjects/loader/merge.py +287 -0
- metaobjects/loader/meta_data_loader.py +245 -0
- metaobjects/loader/sources/__init__.py +24 -0
- metaobjects/loader/sources/directory_source.py +50 -0
- metaobjects/loader/sources/file_source.py +41 -0
- metaobjects/loader/sources/meta_data_source.py +67 -0
- metaobjects/loader/sources/uri_source.py +56 -0
- metaobjects/loader/validate_discriminator.py +181 -0
- metaobjects/loader/validate_field_readonly.py +146 -0
- metaobjects/loader/validate_source_parameter_ref.py +159 -0
- metaobjects/loader/validate_source_physical_names.py +140 -0
- metaobjects/loader/validation_passes.py +1513 -0
- metaobjects/meta/__init__.py +1 -0
- metaobjects/meta/core/__init__.py +0 -0
- metaobjects/meta/core/attr/__init__.py +0 -0
- metaobjects/meta/core/attr/attr_constants.py +31 -0
- metaobjects/meta/core/attr/meta_attr.py +136 -0
- metaobjects/meta/core/field/__init__.py +0 -0
- metaobjects/meta/core/field/field_constants.py +105 -0
- metaobjects/meta/core/field/meta_field.py +76 -0
- metaobjects/meta/core/identity/__init__.py +0 -0
- metaobjects/meta/core/identity/identity_constants.py +19 -0
- metaobjects/meta/core/identity/meta_identity.py +8 -0
- metaobjects/meta/core/object/__init__.py +0 -0
- metaobjects/meta/core/object/meta_object.py +65 -0
- metaobjects/meta/core/object/meta_object_aware.py +43 -0
- metaobjects/meta/core/object/object_class_registry.py +56 -0
- metaobjects/meta/core/object/object_constants.py +13 -0
- metaobjects/meta/core/object/object_extract.py +400 -0
- metaobjects/meta/core/object/value_object.py +70 -0
- metaobjects/meta/core/relationship/__init__.py +0 -0
- metaobjects/meta/core/relationship/derive_m2m_fields.py +180 -0
- metaobjects/meta/core/relationship/meta_relationship.py +54 -0
- metaobjects/meta/core/relationship/relationship_constants.py +51 -0
- metaobjects/meta/core/validator/__init__.py +0 -0
- metaobjects/meta/core/validator/validator_constants.py +18 -0
- metaobjects/meta/meta_data.py +206 -0
- metaobjects/meta/meta_root.py +8 -0
- metaobjects/meta/persistence/__init__.py +0 -0
- metaobjects/meta/persistence/db/__init__.py +1 -0
- metaobjects/meta/persistence/db/db_constants.py +41 -0
- metaobjects/meta/persistence/db/db_provider.py +60 -0
- metaobjects/meta/persistence/origin/__init__.py +0 -0
- metaobjects/meta/persistence/origin/meta_origin.py +8 -0
- metaobjects/meta/persistence/origin/origin_constants.py +20 -0
- metaobjects/meta/persistence/source/__init__.py +0 -0
- metaobjects/meta/persistence/source/meta_source.py +137 -0
- metaobjects/meta/persistence/source/source_constants.py +115 -0
- metaobjects/meta/presentation/__init__.py +0 -0
- metaobjects/meta/presentation/layout/__init__.py +0 -0
- metaobjects/meta/presentation/layout/layout_constants.py +13 -0
- metaobjects/meta/presentation/layout/meta_layout.py +8 -0
- metaobjects/meta/presentation/view/__init__.py +0 -0
- metaobjects/meta/presentation/view/meta_view.py +8 -0
- metaobjects/meta/presentation/view/view_constants.py +22 -0
- metaobjects/meta/template/__init__.py +0 -0
- metaobjects/meta/template/meta_template.py +46 -0
- metaobjects/meta/template/template_constants.py +112 -0
- metaobjects/meta/template/template_provider.py +43 -0
- metaobjects/parser.py +380 -0
- metaobjects/parser_yaml.py +82 -0
- metaobjects/provider.py +111 -0
- metaobjects/py.typed +0 -0
- metaobjects/registry.py +210 -0
- metaobjects/registry_manifest.py +223 -0
- metaobjects/render/__init__.py +74 -0
- metaobjects/render/email_document.py +14 -0
- metaobjects/render/escapers.py +109 -0
- metaobjects/render/extract/__init__.py +59 -0
- metaobjects/render/extract/coerce.py +279 -0
- metaobjects/render/extract/extract.py +211 -0
- metaobjects/render/extract/extract_map.py +61 -0
- metaobjects/render/extract/json_forgiving_reader.py +203 -0
- metaobjects/render/extract/locate.py +65 -0
- metaobjects/render/extract/normalize.py +96 -0
- metaobjects/render/extract/strip.py +20 -0
- metaobjects/render/extract/types.py +332 -0
- metaobjects/render/extract/xml_forgiving_reader.py +162 -0
- metaobjects/render/filesystem_provider.py +51 -0
- metaobjects/render/prompt/__init__.py +32 -0
- metaobjects/render/prompt/output_format_renderer.py +340 -0
- metaobjects/render/prompt/output_format_spec.py +28 -0
- metaobjects/render/prompt/prompt_field.py +29 -0
- metaobjects/render/prompt/prompt_overrides.py +29 -0
- metaobjects/render/prompt/prompt_style.py +38 -0
- metaobjects/render/renderer.py +358 -0
- metaobjects/render/verify.py +266 -0
- metaobjects/runtime/__init__.py +39 -0
- metaobjects/runtime/llm_recorder.py +210 -0
- metaobjects/runtime/n2m_resolver.py +155 -0
- metaobjects/runtime/object_manager.py +715 -0
- metaobjects/runtime/tph.py +50 -0
- metaobjects/serializer_json.py +172 -0
- metaobjects/shared/__init__.py +0 -0
- metaobjects/shared/base_types.py +16 -0
- metaobjects/shared/separators.py +4 -0
- metaobjects/shared/structural.py +9 -0
- metaobjects/source/__init__.py +79 -0
- metaobjects/source/error_source.py +266 -0
- metaobjects/source/json_path.py +106 -0
- metaobjects/source/semantic_diff.py +98 -0
- metaobjects/source/yaml_positions.py +174 -0
- metaobjects/super_resolve.py +128 -0
- metaobjects/yaml_desugar.py +481 -0
- metaobjects-0.9.0.dist-info/METADATA +97 -0
- metaobjects-0.9.0.dist-info/RECORD +181 -0
- metaobjects-0.9.0.dist-info/WHEEL +4 -0
- metaobjects-0.9.0.dist-info/entry_points.txt +2 -0
- metaobjects-0.9.0.dist-info/licenses/LICENSE +189 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Java server runtime
|
|
2
|
+
|
|
3
|
+
The Java runtime tier is **OMDB** (`metaobjects-omdb`) —
|
|
4
|
+
`com.metaobjects.manager.db.ObjectManagerDB`, a metadata-driven persistence engine
|
|
5
|
+
on modernized JDBC + Spring-tx. It reads the same metadata at runtime and drives
|
|
6
|
+
CRUD with no per-entity ORM boilerplate. OMDB is pure data-access (CRUD / query /
|
|
7
|
+
codec / transactions); schema is owned by the Node `meta` migration tool, not OMDB.
|
|
8
|
+
|
|
9
|
+
## Construct an `ObjectManagerDB`
|
|
10
|
+
|
|
11
|
+
`ObjectManagerDB` has a no-arg constructor. Set its `DataSource` (and a
|
|
12
|
+
`DatabaseDriver` for the dialect), then call `init()`:
|
|
13
|
+
|
|
14
|
+
```java
|
|
15
|
+
import com.metaobjects.manager.db.ObjectManagerDB;
|
|
16
|
+
import com.metaobjects.manager.db.ObjectConnection;
|
|
17
|
+
import com.metaobjects.manager.db.driver.PostgresDriver;
|
|
18
|
+
import com.metaobjects.object.MetaObject;
|
|
19
|
+
import com.metaobjects.object.ValueObject;
|
|
20
|
+
|
|
21
|
+
import javax.sql.DataSource;
|
|
22
|
+
import java.util.Collection;
|
|
23
|
+
|
|
24
|
+
ObjectManagerDB om = new ObjectManagerDB();
|
|
25
|
+
om.setDatabaseDriver(new PostgresDriver());
|
|
26
|
+
om.setDataSource(/* javax.sql.DataSource */);
|
|
27
|
+
om.init();
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## CRUD + query
|
|
31
|
+
|
|
32
|
+
Every CRUD/query call takes an `ObjectConnection` obtained from
|
|
33
|
+
`om.getConnection()`; release it with `om.releaseConnection(oc)` when done. Objects
|
|
34
|
+
are `ValueObject` instances created from a `MetaObject` (look the `MetaObject` up by
|
|
35
|
+
its fully-qualified name on the loader's registry).
|
|
36
|
+
|
|
37
|
+
```java
|
|
38
|
+
ObjectConnection oc = om.getConnection();
|
|
39
|
+
try {
|
|
40
|
+
MetaObject mo = registry.findMetaObjectByName("acme::blog::Author");
|
|
41
|
+
|
|
42
|
+
// Create
|
|
43
|
+
ValueObject author = (ValueObject) mo.newInstance();
|
|
44
|
+
author.setString("name", "Ada");
|
|
45
|
+
om.createObject(oc, author);
|
|
46
|
+
|
|
47
|
+
// Load (refresh an object's state by its identity)
|
|
48
|
+
om.loadObject(oc, author);
|
|
49
|
+
|
|
50
|
+
// Update
|
|
51
|
+
author.setString("name", "Ada Lovelace");
|
|
52
|
+
om.updateObject(oc, author);
|
|
53
|
+
|
|
54
|
+
// Query — all rows of the MetaObject
|
|
55
|
+
Collection<?> all = om.getObjects(oc, mo);
|
|
56
|
+
|
|
57
|
+
// Delete
|
|
58
|
+
om.deleteObject(oc, author);
|
|
59
|
+
} finally {
|
|
60
|
+
om.releaseConnection(oc);
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`getObjects` also has a filtered overload `om.getObjects(oc, mo, queryOptions)`
|
|
65
|
+
taking a `QueryOptions` (built from an `Expression`). `ValueObject` is the
|
|
66
|
+
map-backed runtime carrier.
|
|
67
|
+
|
|
68
|
+
## Spring wiring
|
|
69
|
+
|
|
70
|
+
`metaobjects-core-spring` (or the Spring Boot starter) declares an
|
|
71
|
+
`ObjectManagerDB` bean from the Spring `DataSource` and enrolls it in Spring-managed
|
|
72
|
+
transactions — annotate your service methods `@Transactional` and OMDB participates.
|
|
73
|
+
Inject the bean into your services rather than constructing it by hand.
|
|
74
|
+
|
|
75
|
+
## Return-type contract
|
|
76
|
+
|
|
77
|
+
OMDB returns **native in-process Java types**, never wire strings:
|
|
78
|
+
|
|
79
|
+
- `field.decimal` → `java.math.BigDecimal` — exact, **lossless end-to-end**, no
|
|
80
|
+
float round-tripping.
|
|
81
|
+
- temporal fields → native date/instant types.
|
|
82
|
+
- `field.object` (jsonb) → a native `Map`.
|
|
83
|
+
|
|
84
|
+
Wire canonicalization (currency → integer minor units as `Long`, temporals →
|
|
85
|
+
ISO-8601, UUID → canonical hex) happens only when a row leaves over HTTP — at the
|
|
86
|
+
serialization boundary in your Spring controllers — never inside the OMDB query
|
|
87
|
+
path. Compute with `BigDecimal` in-process; let the HTTP layer encode.
|
|
88
|
+
|
|
89
|
+
## Serving the REST contract
|
|
90
|
+
|
|
91
|
+
`codegen-spring`'s `SpringControllerGenerator` emits `<Entity>Controller.java` per
|
|
92
|
+
writable entity, on the cross-port REST contract (five CRUD endpoints, `?sort`,
|
|
93
|
+
`?limit`/`?offset`, `?withCount=1` envelope, 404/400 envelopes). The generated
|
|
94
|
+
`<Entity>Repository.java` is a stubbed interface you implement against OMDB (or any
|
|
95
|
+
persistence layer) — wire the controller to call it. The same universal TS/Angular
|
|
96
|
+
web client consumes those controllers unchanged.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Kotlin server runtime
|
|
2
|
+
|
|
3
|
+
The Kotlin port runtime is **generated Exposed `Table` objects plus your own
|
|
4
|
+
JetBrains Exposed transactions** — there is no Kotlin-specific persistence engine.
|
|
5
|
+
`KotlinExposedTableGenerator` emits one `<Entity>Table.kt` per entity with a
|
|
6
|
+
`source.rdb`; you hand-write the (trivial) transaction bodies, since the table
|
|
7
|
+
column definitions and the `@Serializable` entity data class are both generated
|
|
8
|
+
from the same metadata.
|
|
9
|
+
|
|
10
|
+
(If you want a fully metadata-driven engine instead of hand-written Exposed, the
|
|
11
|
+
Java **OMDB** runtime — `metaobjects-omdb`, `ObjectManagerDB` — is on the JVM and
|
|
12
|
+
callable from Kotlin; see the Java runtime reference. OMDB is pure data-access:
|
|
13
|
+
CRUD / query / codec / transactions. Schema is owned by the Node `meta` migration
|
|
14
|
+
tool, not the runtime.)
|
|
15
|
+
|
|
16
|
+
## The generated table
|
|
17
|
+
|
|
18
|
+
For an `Author` entity, `KotlinEntityGenerator` + `KotlinExposedTableGenerator`
|
|
19
|
+
emit a data class and an Exposed `Table`:
|
|
20
|
+
|
|
21
|
+
```kotlin
|
|
22
|
+
// generated/acme/blog/Author.kt
|
|
23
|
+
@Serializable
|
|
24
|
+
data class Author(
|
|
25
|
+
val id: Long,
|
|
26
|
+
val name: String,
|
|
27
|
+
val bio: String? = null,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
// generated/acme/blog/AuthorTable.kt
|
|
31
|
+
object AuthorTable : Table("authors") {
|
|
32
|
+
val id = long("id").autoIncrement()
|
|
33
|
+
val name = varchar("name", 200)
|
|
34
|
+
val bio = varchar("bio", 2000).nullable()
|
|
35
|
+
override val primaryKey = PrimaryKey(id)
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Query + persist with Exposed
|
|
40
|
+
|
|
41
|
+
Obtain a `Database` (the generated `MetadataExposedConfig` `@Configuration` calls
|
|
42
|
+
`Database.connect(...)` for you, or do it yourself), then wrap reads/writes in
|
|
43
|
+
`transaction(db) { ... }`:
|
|
44
|
+
|
|
45
|
+
```kotlin
|
|
46
|
+
import org.jetbrains.exposed.sql.Database
|
|
47
|
+
import org.jetbrains.exposed.sql.selectAll
|
|
48
|
+
import org.jetbrains.exposed.sql.insert
|
|
49
|
+
import org.jetbrains.exposed.sql.transactions.transaction
|
|
50
|
+
|
|
51
|
+
@Service
|
|
52
|
+
class AuthorService(private val db: Database) {
|
|
53
|
+
fun list(): List<Author> = transaction(db) {
|
|
54
|
+
AuthorTable.selectAll().map {
|
|
55
|
+
Author(
|
|
56
|
+
id = it[AuthorTable.id],
|
|
57
|
+
name = it[AuthorTable.name],
|
|
58
|
+
bio = it[AuthorTable.bio],
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
fun create(name: String, bio: String? = null): Long = transaction(db) {
|
|
64
|
+
AuthorTable.insert {
|
|
65
|
+
it[AuthorTable.name] = name
|
|
66
|
+
it[AuthorTable.bio] = bio
|
|
67
|
+
} get AuthorTable.id
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Filtered reads use Exposed's `selectAll()` plus a `where { ... }` op tree (e.g.
|
|
73
|
+
`AuthorTable.selectAll().where { AuthorTable.name eq someName }`), exactly as the
|
|
74
|
+
`integration-tests-kotlin` query-conformance runner does against Testcontainers
|
|
75
|
+
Postgres.
|
|
76
|
+
|
|
77
|
+
## Return-type contract
|
|
78
|
+
|
|
79
|
+
An Exposed read yields **native in-process Kotlin/JVM types** at the column, never
|
|
80
|
+
wire strings — this is verified by the port's runtime-return-type test:
|
|
81
|
+
|
|
82
|
+
- `field.decimal` (NUMERIC) → `java.math.BigDecimal` — exact, lossless, no float
|
|
83
|
+
round-tripping.
|
|
84
|
+
- `field.long` → `Long`; other scalars to their native Kotlin types.
|
|
85
|
+
- a `timestamp`-with-tz field → `java.time.Instant` (the metaobjects
|
|
86
|
+
`instantWithTimeZone` `Column<Instant>` path — native temporal, not a String).
|
|
87
|
+
|
|
88
|
+
Wire canonicalization (currency → integer minor units as `Long`, temporals →
|
|
89
|
+
ISO-8601, UUID → canonical hex) happens only when a row leaves over HTTP — at the
|
|
90
|
+
serialization boundary in your Spring controller — never inside the query path.
|
|
91
|
+
Compute with `BigDecimal`/`Instant` in-process; let the HTTP layer encode.
|
|
92
|
+
|
|
93
|
+
## Serving the REST contract
|
|
94
|
+
|
|
95
|
+
`KotlinSpringControllerGenerator` emits `<Entity>Controller.kt` per writable entity
|
|
96
|
+
(`source.rdb` `@kind="table"`) as a Spring `@RestController` on the cross-port REST
|
|
97
|
+
contract (five CRUD endpoints, `?sort`, `?limit`/`?offset`, `?withCount=1`
|
|
98
|
+
envelope). The same universal TS/Angular web client consumes those controllers
|
|
99
|
+
unchanged — the wire format matches the C# and Java backends byte-for-byte.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# React web client
|
|
2
|
+
|
|
3
|
+
`@metaobjectsdev/react` is the browser-side React runtime. It is **universal** — it
|
|
4
|
+
consumes any backend (TS / Java / Kotlin / C# / Python) that speaks the cross-port
|
|
5
|
+
REST contract, not just a TS server. It pairs with the `codegen-ts-react`
|
|
6
|
+
`formFile()` generator, which emits `<Entity>.form.tsx` files that import from this
|
|
7
|
+
package.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @metaobjectsdev/react @metaobjectsdev/runtime-web
|
|
13
|
+
npm install --save-dev @metaobjectsdev/codegen-ts-react
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`@metaobjectsdev/react` peer-deps on `react`, `react-hook-form`,
|
|
17
|
+
`@hookform/resolvers`, and `zod`.
|
|
18
|
+
|
|
19
|
+
## Key exports
|
|
20
|
+
|
|
21
|
+
| Export | Purpose |
|
|
22
|
+
|---|---|
|
|
23
|
+
| `useEntityForm(Entity, InsertSchema)` | React Hook Form bound to a generated Zod insert schema; returns the full `UseFormReturn<T>` plus a `.input.<field>` accessor |
|
|
24
|
+
| `<CurrencyInput>` | controlled bidirectional money input — strips symbol/grouping on focus, re-formats on blur, emits integer minor units (cents) to `onChange` |
|
|
25
|
+
|
|
26
|
+
## Generated forms (`formFile()`)
|
|
27
|
+
|
|
28
|
+
Wire `formFile()` in `metaobjects.config.ts` and `meta gen` emits a
|
|
29
|
+
`<Entity>.form.tsx` per entity. The form spreads `.input.<field>` onto each
|
|
30
|
+
control; every metadata-derived attribute (placeholder, type, aria-label, RHF
|
|
31
|
+
validation rule) rides along automatically:
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
// metaobjects.config.ts
|
|
35
|
+
import { defineConfig } from "@metaobjectsdev/cli";
|
|
36
|
+
import { entityFile, queriesFile, barrel } from "@metaobjectsdev/codegen-ts/generators";
|
|
37
|
+
import { formFile } from "@metaobjectsdev/codegen-ts-react";
|
|
38
|
+
|
|
39
|
+
export default defineConfig({
|
|
40
|
+
outDir: "src/generated",
|
|
41
|
+
apiPrefix: "/api",
|
|
42
|
+
generators: [entityFile(), queriesFile(), barrel(), formFile()],
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
// generated/Author.form.tsx (consumer's view)
|
|
48
|
+
import { useEntityForm } from "@metaobjectsdev/react";
|
|
49
|
+
import { Author, AuthorInsertSchema } from "./Author";
|
|
50
|
+
|
|
51
|
+
export function AuthorForm({ onSubmit }: { onSubmit: (v: AuthorInsert) => void }) {
|
|
52
|
+
const form = useEntityForm(Author, AuthorInsertSchema);
|
|
53
|
+
return (
|
|
54
|
+
<form onSubmit={form.handleSubmit(onSubmit)}>
|
|
55
|
+
<input {...form.input.name} />
|
|
56
|
+
<textarea {...form.input.bio} />
|
|
57
|
+
<button type="submit">Save</button>
|
|
58
|
+
</form>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Validation runs through `zodResolver` against the generated `AuthorInsertSchema`.
|
|
64
|
+
`handleSubmit`, `formState`, `setValue`, etc. are all available since the hook
|
|
65
|
+
returns the full RHF surface.
|
|
66
|
+
|
|
67
|
+
## Currency input
|
|
68
|
+
|
|
69
|
+
`field.currency` stores + transmits integer minor units; the browser formats.
|
|
70
|
+
`<CurrencyInput>` keeps editing native (cents in, cents out); `formatCurrency`
|
|
71
|
+
(from `@metaobjectsdev/runtime-web`) is the display side.
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
import { formatCurrency } from "@metaobjectsdev/runtime-web";
|
|
75
|
+
import { CurrencyInput } from "@metaobjectsdev/react";
|
|
76
|
+
|
|
77
|
+
formatCurrency(1599, "USD", "en-US"); // "$15.99"
|
|
78
|
+
<CurrencyInput value={1599} onChange={setCents} currency="USD" locale="en-US" />
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Talking to the backend
|
|
82
|
+
|
|
83
|
+
React forms submit through whatever data layer you wire. For list/query + mutation
|
|
84
|
+
hooks against the REST contract, add the TanStack client
|
|
85
|
+
(`@metaobjectsdev/tanstack` + `codegen-ts-tanstack`) — see `tanstack.md`. Both sit
|
|
86
|
+
on the single `EntityFetcher` seam your app supplies at its root.
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# TanStack web client
|
|
2
|
+
|
|
3
|
+
`@metaobjectsdev/tanstack` is the browser-side TanStack runtime — TanStack Query
|
|
4
|
+
hooks + a TanStack Table grid component. Like the React client it is **universal**:
|
|
5
|
+
it consumes any backend (TS / Java / Kotlin / C# / Python) that speaks the
|
|
6
|
+
cross-port REST contract. It pairs with `codegen-ts-tanstack`, which emits
|
|
7
|
+
`<Entity>.hooks.ts` and `<Entity>.columns.tsx` that import from this package.
|
|
8
|
+
|
|
9
|
+
## Contents
|
|
10
|
+
- Install
|
|
11
|
+
- Key exports
|
|
12
|
+
- The `EntityFetcher` contract
|
|
13
|
+
- Generated hooks (`tanstackQuery()`)
|
|
14
|
+
- Generated grid (`tanstackGrid()`)
|
|
15
|
+
- Cell renderer overrides
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @metaobjectsdev/tanstack @metaobjectsdev/runtime-web
|
|
21
|
+
npm install --save-dev @metaobjectsdev/codegen-ts-tanstack
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Peer-deps: `@tanstack/react-query`, `@tanstack/react-table`.
|
|
25
|
+
|
|
26
|
+
## Key exports
|
|
27
|
+
|
|
28
|
+
| Export | Purpose |
|
|
29
|
+
|---|---|
|
|
30
|
+
| `<EntityFetcherProvider value={fetcher}>` | supplies the single `EntityFetcher` every generated hook reads |
|
|
31
|
+
| `useEntityFetcher()` | reads the fetcher from context (generated hooks call this) |
|
|
32
|
+
| `<EntityGrid>` | opinionated TanStack Table component |
|
|
33
|
+
| `<CellRendererProvider>` + `defaultCellRenderers` | renderer overrides keyed by the column's `meta.view` |
|
|
34
|
+
|
|
35
|
+
## The `EntityFetcher` contract
|
|
36
|
+
|
|
37
|
+
The client never calls `fetch` directly. Every generated hook delegates to one
|
|
38
|
+
fetcher you supply once at the app root:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
// from @metaobjectsdev/runtime-web
|
|
42
|
+
export type EntityFetcher = <T>(path: string, init?: RequestInit) => Promise<T>;
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
import { EntityFetcherProvider } from "@metaobjectsdev/tanstack";
|
|
47
|
+
|
|
48
|
+
const fetcher = async <T,>(path: string, init?: RequestInit): Promise<T> => {
|
|
49
|
+
const res = await fetch(path, {
|
|
50
|
+
...init,
|
|
51
|
+
credentials: "include",
|
|
52
|
+
headers: { "Content-Type": "application/json", ...(init?.headers ?? {}) },
|
|
53
|
+
});
|
|
54
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} on ${path}`); // hooks rely on the throw
|
|
55
|
+
return res.status === 204 ? (undefined as T) : ((await res.json()) as T);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export function App() {
|
|
59
|
+
return (
|
|
60
|
+
<EntityFetcherProvider value={fetcher}>
|
|
61
|
+
<AuthorList />
|
|
62
|
+
</EntityFetcherProvider>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The fetcher resolves `path` (always starting with `apiPrefix`) to a full URL,
|
|
68
|
+
attaches auth per your policy, parses JSON, and **throws on non-2xx** — the hooks
|
|
69
|
+
depend on the throw for error state.
|
|
70
|
+
|
|
71
|
+
## Generated hooks (`tanstackQuery()`)
|
|
72
|
+
|
|
73
|
+
Emits `<Entity>.hooks.ts` — 5 hooks for a writable entity (2 for read-only
|
|
74
|
+
projections):
|
|
75
|
+
|
|
76
|
+
| Hook | Verb / Path |
|
|
77
|
+
|---|---|
|
|
78
|
+
| `useAuthor(id)` | `GET /api/author/:id` |
|
|
79
|
+
| `useAuthors(filter?)` | `GET /api/author?filter[..]=..&sort=..&limit=N&offset=N` |
|
|
80
|
+
| `useCreateAuthor()` | `POST /api/author` |
|
|
81
|
+
| `useUpdateAuthor()` | `PATCH /api/author/:id` |
|
|
82
|
+
| `useDeleteAuthor()` | `DELETE /api/author/:id` |
|
|
83
|
+
|
|
84
|
+
Query hooks return `UseQueryResult`; mutation hooks return `UseMutationResult` and
|
|
85
|
+
invalidate the entity's query keys so lists re-fetch after writes.
|
|
86
|
+
|
|
87
|
+
## Generated grid (`tanstackGrid()`)
|
|
88
|
+
|
|
89
|
+
Emits `<Entity>.columns.tsx` from the entity's `layout.dataGrid` child — TanStack
|
|
90
|
+
`ColumnDef<T>[]`, each carrying `meta.view` for the renderer registry. Render with
|
|
91
|
+
`<EntityGrid>`:
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
import { useAuthors } from "./generated/Author.hooks";
|
|
95
|
+
import { authorColumns } from "./generated/Author.columns";
|
|
96
|
+
import { EntityGrid } from "@metaobjectsdev/tanstack";
|
|
97
|
+
|
|
98
|
+
const { data } = useAuthors({ sort: "name:asc", limit: 25, offset: 0, withCount: 1 });
|
|
99
|
+
<EntityGrid columns={authorColumns} data={data?.rows ?? []} rowCount={data?.total ?? 0} />
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`tanstackGridHook()` (optional) wraps the sorting/pagination/filter state plumbing
|
|
103
|
+
into a `useAuthorGrid()` so the consumer renders `<EntityGrid {...useAuthorGrid()} />`.
|
|
104
|
+
|
|
105
|
+
## Cell renderer overrides
|
|
106
|
+
|
|
107
|
+
`<EntityGrid>` routes rendering through `CellRendererProvider`, keyed by `meta.view`
|
|
108
|
+
(`text` / `number` / `date` / `boolean` / `currency` / `dropdown` / …). Override a
|
|
109
|
+
key without touching generated code; per-column `cell` always wins, the provider
|
|
110
|
+
fills in otherwise.
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
import { CellRendererProvider } from "@metaobjectsdev/tanstack";
|
|
114
|
+
import { formatCurrency } from "@metaobjectsdev/runtime-web";
|
|
115
|
+
|
|
116
|
+
<CellRendererProvider value={{ currency: (ctx) => formatCurrency(ctx.getValue() as number, "EUR", "fr-FR") }}>
|
|
117
|
+
<EntityGrid {...gridProps} />
|
|
118
|
+
</CellRendererProvider>
|
|
119
|
+
```
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# TypeScript server runtime
|
|
2
|
+
|
|
3
|
+
The Node-side runtime tier is `@metaobjectsdev/runtime-ts`. It supplies both the
|
|
4
|
+
helpers the generated routes lean on (`parseFilterParams`) and a metadata-driven
|
|
5
|
+
`ObjectManager` for full-runtime CRUD / validation / relationship traversal.
|
|
6
|
+
|
|
7
|
+
## Two ways to persist
|
|
8
|
+
|
|
9
|
+
**1. Generated query helpers (the common path).** `queriesFile()` emits a
|
|
10
|
+
`<Entity>.queries.ts` per entity with typed CRUD. Per ADR-0008 every generated
|
|
11
|
+
helper takes the Drizzle/Kysely `db` as its **first parameter** — no module-level
|
|
12
|
+
`db` singleton:
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { findAuthorById, createAuthor, listAuthors } from "./generated/Author.queries";
|
|
16
|
+
|
|
17
|
+
const author = await findAuthorById(db, 42); // db passed, not imported
|
|
18
|
+
const created = await createAuthor(db, { name: "Ada" });
|
|
19
|
+
const page = await listAuthors(db, { limit: 25 });
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
You own the connection lifecycle and thread `db` through every call — that keeps
|
|
23
|
+
the code testable and lets one process talk to multiple databases.
|
|
24
|
+
|
|
25
|
+
**2. The `ObjectManager` runtime (dynamic CRUD / admin UIs / MCP tools).** Drives
|
|
26
|
+
behavior directly off loaded metadata, no per-entity generated file needed:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { MetaDataLoader } from "@metaobjectsdev/metadata";
|
|
30
|
+
import { FileSource } from "@metaobjectsdev/metadata/core";
|
|
31
|
+
import { ObjectManager } from "@metaobjectsdev/runtime-ts";
|
|
32
|
+
import { kyselyDriver } from "@metaobjectsdev/runtime-ts/drivers";
|
|
33
|
+
|
|
34
|
+
const { root } = await new MetaDataLoader().load([
|
|
35
|
+
new FileSource("metaobjects/meta.blog.json"),
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
const om = new ObjectManager({
|
|
39
|
+
metadata: root,
|
|
40
|
+
driver: kyselyDriver({ db: kyselyInstance, dialect: "postgres" }),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const post = await om.create("Post", { title: "Hello", authorId: 1 });
|
|
44
|
+
const found = await om.findById("Post", post.id, { include: ["author"] });
|
|
45
|
+
const list = await om.findMany("Post", { authorId: 1 }, { limit: 10 });
|
|
46
|
+
await om.update("Post", post.id, { title: "Updated" });
|
|
47
|
+
await om.delete("Post", post.id);
|
|
48
|
+
|
|
49
|
+
const result = om.validate("Post", { title: "x" }); // pure, no DB hit
|
|
50
|
+
if (!result.ok) console.log(result.errors);
|
|
51
|
+
|
|
52
|
+
await om.transaction(async (tx) => { /* ... */ });
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Drivers
|
|
56
|
+
|
|
57
|
+
- `kyselyDriver({ db, dialect })` — real DBs (SQLite/libsql/Turso, Postgres via
|
|
58
|
+
`pg` / Neon). You provide the Kysely instance.
|
|
59
|
+
- `inMemoryDriver({ seed?, pkFields? })` — Map-backed; unit tests, prototyping, MCP
|
|
60
|
+
sandboxing.
|
|
61
|
+
|
|
62
|
+
`findMany` filters take a Mongo-style object: `{ field: value }` (eq),
|
|
63
|
+
`{ field: null }` (IS NULL), `{ field: [a, b] }` (IN), or explicit operators
|
|
64
|
+
`{ field: { $gte, $like, $in, ... } }`.
|
|
65
|
+
|
|
66
|
+
> Driver note: generated CRUD uses Kysely's `.returning()`. Works on libsql/Turso,
|
|
67
|
+
> `node-postgres`, `@neondatabase/serverless`; NOT on `better-sqlite3` / `bun:sqlite`
|
|
68
|
+
> (no native RETURNING) — use a custom driver or `inMemoryDriver` there.
|
|
69
|
+
|
|
70
|
+
## Return-type contract
|
|
71
|
+
|
|
72
|
+
The runtime returns **native in-process types**, never wire strings — temporal
|
|
73
|
+
fields as native dates, jsonb as native objects. The one documented TS outlier:
|
|
74
|
+
`field.decimal` comes back as a **`string`** (JS has no native exact decimal),
|
|
75
|
+
preserving precision. Wire canonicalization (currency → integer minor units,
|
|
76
|
+
temporals → ISO-8601, UUID → canonical hex) is applied only at the HTTP
|
|
77
|
+
serialization boundary, never inside the query path.
|
|
78
|
+
|
|
79
|
+
## Serving the REST contract
|
|
80
|
+
|
|
81
|
+
`routesFile()` (Fastify) or `routesFileHono()` (Hono/Workers/edge) emits CRUD
|
|
82
|
+
routes on the cross-port contract. Mount them with the `db` injected:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import { registerAuthorRoutes } from "./generated/Author.routes";
|
|
86
|
+
registerAuthorRoutes(app, { db }); // GET/POST/PATCH/PUT/DELETE under apiPrefix
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The routes call `parseFilterParams` (from `@metaobjectsdev/runtime-ts/drizzle-fastify`)
|
|
90
|
+
to validate `?filter[..][..]=..&sort=..&limit=&offset=` against the generated
|
|
91
|
+
`<Entity>FilterAllowlist` / `<Entity>SortAllowlist`, returning HTTP 400 on an
|
|
92
|
+
unknown field or disallowed operator.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: metaobjects-verify
|
|
3
|
+
description: Use when verifying MetaObjects: drift checks (verify --db/--codegen/--templates), schema migrations, and interpreting conformance/test failures.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# MetaObjects verify + migrations
|
|
7
|
+
|
|
8
|
+
The third pillar: **drift detection.** MetaObjects treats metadata as the source of
|
|
9
|
+
truth and generated code + DB schema + prompts as derived. `verify` is the
|
|
10
|
+
cross-cutting discipline that catches divergence; schema migration is the build-time
|
|
11
|
+
pipeline that brings the database into line with metadata. This skill is the
|
|
12
|
+
procedure for running both and reading the failures.
|
|
13
|
+
|
|
14
|
+
## The drift sources
|
|
15
|
+
|
|
16
|
+
Drift is any place where a derived artifact has fallen out of sync with the
|
|
17
|
+
metadata that should define it. The ones a developer must actively guard:
|
|
18
|
+
|
|
19
|
+
- **DB-vs-metadata** — the live database schema has diverged from the metadata
|
|
20
|
+
(a column the metadata no longer declares, a missing index, a type mismatch).
|
|
21
|
+
- **Generated-vs-metadata (codegen)** — committed generated code no longer matches
|
|
22
|
+
what the current metadata would emit (someone edited a `@generated` file, or
|
|
23
|
+
forgot to regenerate after changing metadata).
|
|
24
|
+
- **Prompt-vs-payload (templates)** — a template references a `{{field}}` that
|
|
25
|
+
isn't on its `@payloadRef` payload VO (a renamed source field silently degrading
|
|
26
|
+
a prompt).
|
|
27
|
+
|
|
28
|
+
Two more are caught structurally rather than by a command: **generated-edited**
|
|
29
|
+
(the `@generated` header + three-way merge surface hand-edits at code review) and
|
|
30
|
+
**migration-vs-metadata** (migrations are emitted *from* metadata diffs, so they
|
|
31
|
+
can't drift by construction).
|
|
32
|
+
|
|
33
|
+
## The `verify` subverbs
|
|
34
|
+
|
|
35
|
+
`verify` has three drift checks. Run them in CI.
|
|
36
|
+
|
|
37
|
+
- **`--db`** — schema drift. Introspects the live database and fails if it has
|
|
38
|
+
diverged from metadata. This is a **schema concern, so it is the Node toolchain's
|
|
39
|
+
job regardless of your server language** (see migrations below). On the JVM ports
|
|
40
|
+
a runtime startup validator catches generated-table drift at app boot as a
|
|
41
|
+
complementary check, but the authoritative DB-vs-metadata gate is the Node
|
|
42
|
+
`verify --db`.
|
|
43
|
+
|
|
44
|
+
- **`--codegen`** — regeneration drift. Re-runs generation and diffs the result
|
|
45
|
+
against the committed generated files; a non-empty diff means someone edited
|
|
46
|
+
generated code or skipped a regen. Wire it into CI so a stale `@generated` file
|
|
47
|
+
fails the build.
|
|
48
|
+
|
|
49
|
+
- **`--templates`** — prompt/payload drift. For every `template.prompt` /
|
|
50
|
+
`template.output`, resolves the text, parses each `{{...}}` reference, and fails
|
|
51
|
+
if any reference isn't on the payload VO. This is the build-time gate for the
|
|
52
|
+
prompt-construction pillar.
|
|
53
|
+
|
|
54
|
+
A clean run is silent; a failure names the entity/template, the drifted artifact,
|
|
55
|
+
and (for templates) the missing reference. **Bias toward trusting the tool** — a
|
|
56
|
+
verify failure almost always means the metadata changed and a derived artifact
|
|
57
|
+
didn't follow.
|
|
58
|
+
|
|
59
|
+
## Schema migrations are the shared TypeScript engine — for every port
|
|
60
|
+
|
|
61
|
+
This is the load-bearing architectural fact (ADR-0015): **schema migrations are
|
|
62
|
+
owned by one shared TypeScript engine, regardless of your server language.** The
|
|
63
|
+
Node `meta migrate` and `meta verify --db` are the migration + live-DB-drift
|
|
64
|
+
toolchain for TS, Java, Kotlin, C#, and Python alike.
|
|
65
|
+
|
|
66
|
+
What this means in practice:
|
|
67
|
+
|
|
68
|
+
- The Node `meta` CLI emits the migration SQL (diffing metadata → DDL) and applies
|
|
69
|
+
it. You point it at the same database your server connects to:
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
meta migrate --db postgresql://... --slug initial # emit migration SQL
|
|
73
|
+
meta migrate --db postgresql://... --apply # apply pending migrations
|
|
74
|
+
meta migrate --dry-run # preview without writing
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
- Dialects: `postgres` (default), `sqlite`, and `d1` (Cloudflare D1, TS-only).
|
|
78
|
+
- The JVM and Python ports have **no** migration command of their own — their
|
|
79
|
+
former migrate goals/modules were removed. A JVM service may auto-create
|
|
80
|
+
dev/test tables at startup for convenience, but production schema is always the
|
|
81
|
+
Node migrate engine's output.
|
|
82
|
+
|
|
83
|
+
So even in a Java or Python or C# project, schema migration and `verify --db` run
|
|
84
|
+
through the Node `meta` tool. The per-port `gen`/codegen tooling stays native to
|
|
85
|
+
the language; only schema crosses to Node.
|
|
86
|
+
|
|
87
|
+
## Interpreting conformance / test failures
|
|
88
|
+
|
|
89
|
+
MetaObjects' behavior is pinned by cross-port **conformance corpora** (metamodel,
|
|
90
|
+
render, persistence, API-contract, verify). When a test or conformance fixture
|
|
91
|
+
fails:
|
|
92
|
+
|
|
93
|
+
- A **loader** failure cites an `ERR_*` code (e.g. `ERR_RESERVED_ATTR`,
|
|
94
|
+
`ERR_UNKNOWN_EXTENDS`, `ERR_MISSING_REQUIRED_ATTR`, `ERR_BAD_ATTR_VALUE`,
|
|
95
|
+
`ERR_YAML_COERCION`) — fix the metadata, not the loader.
|
|
96
|
+
- A **render/verify** failure means the rendered bytes or the template-drift
|
|
97
|
+
result diverged from the pinned expectation — usually a payload/text mismatch.
|
|
98
|
+
- A **persistence / API-contract** failure means a query result row or an HTTP
|
|
99
|
+
response shape diverged from the cross-port expectation — treat a deviation as a
|
|
100
|
+
bug in the code under test, not in the corpus.
|
|
101
|
+
|
|
102
|
+
The corpus is the contract: when output disagrees with a fixture, the output is
|
|
103
|
+
what's wrong.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
For the migration tooling read `references/migration.md`.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Schema migrations — the shared TypeScript engine (every port)
|
|
2
|
+
|
|
3
|
+
Schema migration is owned by **one shared TypeScript engine** regardless of your
|
|
4
|
+
server language (ADR-0015). The Node `meta` CLI (`@metaobjectsdev/cli`, on top of
|
|
5
|
+
`@metaobjectsdev/migrate-ts`) is the migration + live-DB-drift toolchain for **TS,
|
|
6
|
+
Java, Kotlin, C#, and Python alike**. The non-TS ports have **no** migration command
|
|
7
|
+
of their own — their former migrate goals/modules were removed. A JVM service may
|
|
8
|
+
auto-create dev/test tables at startup for convenience, but production schema is
|
|
9
|
+
always the Node migrate engine's output.
|
|
10
|
+
|
|
11
|
+
So even in a Java / Python / C# / Kotlin project you run `meta migrate` and
|
|
12
|
+
`meta verify --db` through Node. Only schema crosses to Node; per-port `gen`/codegen
|
|
13
|
+
stays native to the language.
|
|
14
|
+
|
|
15
|
+
## Install (Node, dev-only)
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install --save-dev @metaobjectsdev/cli @metaobjectsdev/migrate-ts
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
You point the tool at the **same database your server connects to** — its
|
|
22
|
+
connection is independent of your runtime tier.
|
|
23
|
+
|
|
24
|
+
## The workflow
|
|
25
|
+
|
|
26
|
+
1. **Generate a migration** by diffing metadata vs the prior state (the live DB or a
|
|
27
|
+
committed snapshot). The engine emits paired `up.sql` + `down.sql`:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
meta migrate --db postgresql://... # emit up.sql + down.sql
|
|
31
|
+
meta migrate --db postgresql://... --slug initial # name the migration
|
|
32
|
+
meta migrate --dry-run # preview without writing
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
2. **Review the SQL.** Read the emitted `up.sql` (forward) and `down.sql`
|
|
36
|
+
(rollback) before applying. Destructive changes (drop column / drop table) are
|
|
37
|
+
opt-in — the engine blocks them unless explicitly allowed, and routes ambiguous
|
|
38
|
+
rename-vs-drop+add decisions through a prompt rather than guessing.
|
|
39
|
+
|
|
40
|
+
3. **Apply** the pending migrations against the DB; migration history is tracked in
|
|
41
|
+
a ledger table:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
meta migrate --db postgresql://... --apply # run pending up.sql
|
|
45
|
+
meta migrate --db postgresql://... --rollback # run down.sql for the last migration
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Dialects
|
|
49
|
+
|
|
50
|
+
- `postgres` (default) — native `ALTER`s.
|
|
51
|
+
- `sqlite` (libsql / Turso) — native `ALTER`s where supported (≥ 3.35), bundling
|
|
52
|
+
recreate-and-copy per table when a change needs it.
|
|
53
|
+
- `d1` (Cloudflare D1) — **TS-only**; targets D1 via the wrangler CLI, writes
|
|
54
|
+
Wrangler's native `migrations/<seq>_<slug>.sql` layout. Pass `--dialect d1`.
|
|
55
|
+
|
|
56
|
+
## Live-DB drift: `meta verify --db`
|
|
57
|
+
|
|
58
|
+
`meta verify --db` introspects the live database and fails if its schema has
|
|
59
|
+
diverged from the metadata (a column the metadata no longer declares, a missing
|
|
60
|
+
index, a type mismatch). This is the **authoritative** DB-vs-metadata gate for every
|
|
61
|
+
port — wire it into CI. On the JVM ports a runtime startup validator can catch
|
|
62
|
+
generated-table drift at app boot as a complementary check, but the gate that owns
|
|
63
|
+
DB drift is the Node `meta verify --db`.
|
|
64
|
+
|
|
65
|
+
A clean run is silent; a failure names the drifted table/column. Bias toward
|
|
66
|
+
trusting the tool — a drift failure almost always means the metadata changed and the
|
|
67
|
+
DB didn't follow.
|
|
68
|
+
|
|
69
|
+
## Not yet shipped
|
|
70
|
+
|
|
71
|
+
Triggers, generated columns, partial/exclusion/check constraints, MySQL, and data
|
|
72
|
+
migrations (column-type changes needing data transformation error out with a hint).
|