feat: publish Dever core with project scaffolding and versioned AI skills
This commit is contained in:
1 parent
70fa73cdc2
commit
ac458ffa7f
960 files changed
+221319
-1163
No files matched your search
@@ -0,0 +1,549 @@
|
||||
use dever_core::source::SourceMap;
|
||||
use dever_runtime::config::RuntimeProfile;
|
||||
|
||||
const MODEL: &str = r#"
|
||||
public type UserStatus {
|
||||
Active = "启用"
|
||||
Disabled = "禁用"
|
||||
}
|
||||
|
||||
public type User {
|
||||
external_uuid: Uuid default "018f08d8-59b0-7cc2-98c4-dc0c0c07398f"
|
||||
email: Text(254) unique
|
||||
display_name: Text(1, 64)
|
||||
status: UserStatus default UserStatus.Active index
|
||||
enabled: Bool default true
|
||||
age: Int default 0
|
||||
alias: Text?
|
||||
}
|
||||
"#;
|
||||
|
||||
const SERVICE: &str = r#"
|
||||
public register() (user: user.model.user.User) {
|
||||
user = user.create({
|
||||
external_uuid = "018f08d8-59b0-7cc2-98c4-dc0c0c07398f"
|
||||
email = "reader@example.com"
|
||||
display_name = "Reader"
|
||||
})
|
||||
}
|
||||
|
||||
public active() (users: user.model.user.Page) {
|
||||
users = user.list({
|
||||
where = status == user.model.user.UserStatus.Active and enabled == true
|
||||
order = [created_at.desc]
|
||||
page = 2
|
||||
size = 10
|
||||
})
|
||||
}
|
||||
|
||||
public find(id: user.model.user.id) (user: user.model.user.User) {
|
||||
user = user.get(id)
|
||||
}
|
||||
|
||||
public disable(id: user.model.user.id) (changed: Int) {
|
||||
changed = user.update(id, {
|
||||
status = user.model.user.UserStatus.Disabled
|
||||
enabled = false
|
||||
})
|
||||
}
|
||||
|
||||
public remove_disabled() (changed: Int) {
|
||||
changed = user.delete({
|
||||
where = status == user.model.user.UserStatus.Disabled
|
||||
})
|
||||
}
|
||||
|
||||
public bulk() (changed: Int) {
|
||||
changed = user.create_many([
|
||||
{
|
||||
email = "one@example.com"
|
||||
display_name = "One"
|
||||
},
|
||||
{
|
||||
email = "two@example.com"
|
||||
display_name = "Two"
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
public advanced() (users: user.model.user.Page) {
|
||||
users = user.list({
|
||||
where = not contains(display_name, "bot") and in(status, [user.model.user.UserStatus.Active, user.model.user.UserStatus.Disabled]) and between(age, 18, 65) and starts_with(email, "reader") and ends_with(email, ".com") and alias == null
|
||||
})
|
||||
}
|
||||
|
||||
public page_after() (users: user.model.user.Cursor) {
|
||||
users = user.cursor({
|
||||
after = null
|
||||
order = [created_at.desc, id.desc]
|
||||
size = 10
|
||||
})
|
||||
}
|
||||
|
||||
show(user: user.model.user.User) () {}
|
||||
|
||||
public scan() () {
|
||||
users = user.stream({ size = 4 })
|
||||
each(show, users)
|
||||
}
|
||||
|
||||
public save() (saved: user.model.user.User) {
|
||||
saved = user.upsert(
|
||||
{ email = "reader@example.com" },
|
||||
{ display_name = "Reader" },
|
||||
{ display_name = "Updated" }
|
||||
)
|
||||
}
|
||||
"#;
|
||||
|
||||
fn sources(service: &str) -> SourceMap {
|
||||
let mut sources = SourceMap::default();
|
||||
sources.add("user/model/user.dever", MODEL);
|
||||
sources.add("user/service/account.dever", service);
|
||||
sources
|
||||
}
|
||||
|
||||
fn errors(service: &str) -> String {
|
||||
let sources = sources(service);
|
||||
dever_core::check(&sources)
|
||||
.unwrap_err()
|
||||
.iter()
|
||||
.map(|error| error.render(&sources))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_operations_build_static_query_plans_and_parameterized_sql() {
|
||||
let sources = sources(SERVICE);
|
||||
let program = dever_core::check(&sources).unwrap();
|
||||
let api = program.api_snapshot();
|
||||
assert!(api.contains("function user.service.account.register"));
|
||||
assert!(api.contains("effects=[database]"), "{api}");
|
||||
|
||||
let create =
|
||||
dever_core::native::emit(&program, &sources, "user.service.account.register").unwrap();
|
||||
assert!(create.contains("INSERT INTO \\\"user\\\""), "{create}");
|
||||
assert!(create.contains("VALUES (?1, ?2, ?3)"), "{create}");
|
||||
assert!(
|
||||
create.contains("dever_runtime::orm::Uuid::from_bytes"),
|
||||
"{create}"
|
||||
);
|
||||
assert!(
|
||||
create.contains("dever_runtime::orm::Value::Uuid"),
|
||||
"{create}"
|
||||
);
|
||||
assert!(create.contains("$1::uuid"), "{create}");
|
||||
assert!(create.contains("decode_model_"), "{create}");
|
||||
|
||||
let list = dever_core::native::emit(&program, &sources, "user.service.account.active").unwrap();
|
||||
assert!(list.contains("SELECT COUNT(*) FROM \\\"user\\\""), "{list}");
|
||||
assert!(
|
||||
list.contains("ORDER BY \\\"created_at\\\" DESC, \\\"id\\\" DESC"),
|
||||
"{list}"
|
||||
);
|
||||
assert!(list.contains("LIMIT ?3 OFFSET ?4"), "{list}");
|
||||
|
||||
let advanced =
|
||||
dever_core::native::emit(&program, &sources, "user.service.account.advanced").unwrap();
|
||||
assert!(
|
||||
advanced.contains("NOT (instr(\\\"display_name\\\", ?1) > 0)"),
|
||||
"{advanced}"
|
||||
);
|
||||
assert!(
|
||||
advanced.contains("\\\"status\\\" IN (?2, ?3)"),
|
||||
"{advanced}"
|
||||
);
|
||||
assert!(
|
||||
advanced.contains("\\\"age\\\" BETWEEN ?4 AND ?5"),
|
||||
"{advanced}"
|
||||
);
|
||||
assert!(advanced.contains("\\\"alias\\\" IS NULL"), "{advanced}");
|
||||
|
||||
let cursor =
|
||||
dever_core::native::emit(&program, &sources, "user.service.account.page_after").unwrap();
|
||||
assert!(
|
||||
cursor.contains("(CAST(?1 AS BIGINT) IS NULL OR ((\\\"created_at\\\" < ?1) OR (\\\"created_at\\\" IS ?1 AND \\\"id\\\" < ?2)))"),
|
||||
"{cursor}"
|
||||
);
|
||||
assert!(
|
||||
cursor.contains("(CAST($1 AS BIGINT) IS NULL OR ((\\\"created_at\\\" < $1) OR (\\\"created_at\\\" IS NOT DISTINCT FROM $1 AND \\\"id\\\" < $2)))"),
|
||||
"{cursor}"
|
||||
);
|
||||
assert!(cursor.contains("LIMIT ?3"), "{cursor}");
|
||||
assert!(!cursor.contains("COUNT(*)"), "{cursor}");
|
||||
|
||||
let bulk = dever_core::native::emit(&program, &sources, "user.service.account.bulk").unwrap();
|
||||
assert!(bulk.contains("VALUES (?1, ?2), (?3, ?4)"), "{bulk}");
|
||||
|
||||
let stream = dever_core::native::emit(&program, &sources, "user.service.account.scan").unwrap();
|
||||
assert!(stream.contains("database.stream("), "{stream}");
|
||||
assert!(stream.contains("ORDER BY \\\"id\\\" DESC"), "{stream}");
|
||||
assert!(
|
||||
!stream.contains("collect::<Result<Vec<_>, _>>()"),
|
||||
"{stream}"
|
||||
);
|
||||
|
||||
let upsert = dever_core::native::emit(&program, &sources, "user.service.account.save").unwrap();
|
||||
assert!(
|
||||
upsert.contains("ON CONFLICT (\\\"email\\\") DO UPDATE"),
|
||||
"{upsert}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_operations_reject_unknown_fields_and_invalid_query_shapes() {
|
||||
let unknown = SERVICE.replace(
|
||||
"email = \"reader@example.com\"",
|
||||
"unknown = \"reader@example.com\"",
|
||||
);
|
||||
assert!(errors(&unknown).contains("unknown Model field 'unknown'"));
|
||||
|
||||
let generated = SERVICE.replace(
|
||||
"email = \"reader@example.com\"",
|
||||
"id = \"reader@example.com\"",
|
||||
);
|
||||
assert!(errors(&generated).contains("generated Model field 'id'"));
|
||||
|
||||
let unknown_option = SERVICE.replace("page = 2", "offset = 2");
|
||||
assert!(errors(&unknown_option).contains("unknown query option 'offset'"));
|
||||
|
||||
let invalid_in = SERVICE.replace(
|
||||
"in(status, [user.model.user.UserStatus.Active, user.model.user.UserStatus.Disabled])",
|
||||
"in(status, [])",
|
||||
);
|
||||
assert!(errors(&invalid_in).contains("between 1 and 100 values"));
|
||||
|
||||
let invalid_text = SERVICE.replace("contains(display_name, \"bot\")", "contains(age, \"bot\")");
|
||||
assert!(errors(&invalid_text).contains("requires a Text field"));
|
||||
|
||||
let invalid_arity =
|
||||
SERVICE.replace("contains(display_name, \"bot\")", "contains(display_name)");
|
||||
assert!(errors(&invalid_arity).contains("where 'contains' expects 2 arguments"));
|
||||
|
||||
let invalid_field = SERVICE.replace(
|
||||
"contains(display_name, \"bot\")",
|
||||
"contains(\"bot\", display_name)",
|
||||
);
|
||||
assert!(errors(&invalid_field).contains("the first 'contains' argument must be a Model field"));
|
||||
|
||||
let outside_where = SERVICE.replace(
|
||||
"user = user.get(id)",
|
||||
"value = contains(\"reader\", \"read\")\n user = user.get(id)",
|
||||
);
|
||||
let outside_where_errors = errors(&outside_where);
|
||||
assert!(
|
||||
outside_where_errors.contains("unknown function 'contains' with 2 argument(s)"),
|
||||
"{outside_where_errors}"
|
||||
);
|
||||
|
||||
let invalid_cursor = SERVICE.replace("order = [created_at.desc, id.desc]", "order = alias.asc");
|
||||
assert!(errors(&invalid_cursor).contains("does not support nullable Model fields"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_create_many_uses_one_valid_statement_per_dialect() {
|
||||
let mut sources = SourceMap::default();
|
||||
sources.add("app/event/model.dever", "type Event {}");
|
||||
sources.add(
|
||||
"app/event/app.dever",
|
||||
"create() (created: Int) { created = model.create_many([{}, {}]) }",
|
||||
);
|
||||
let program = dever_core::check(&sources).unwrap();
|
||||
let native = dever_core::native::emit(&program, &sources, "app.event.create").unwrap();
|
||||
assert!(
|
||||
native.contains("sqlite: \"INSERT INTO \\\"event\\\" (\\\"id\\\") VALUES (NULL), (NULL)\""),
|
||||
"{native}"
|
||||
);
|
||||
assert!(
|
||||
native.contains("postgres: \"INSERT INTO \\\"event\\\" (\\\"created_at\\\") VALUES (DEFAULT), (DEFAULT)\""),
|
||||
"{native}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transaction_functions_generate_one_shared_database_boundary() {
|
||||
let service = SERVICE.replacen("register()", "transaction register()", 1);
|
||||
let sources = sources(&service);
|
||||
let program = dever_core::check(&sources).unwrap();
|
||||
let native =
|
||||
dever_core::native::emit(&program, &sources, "user.service.account.register").unwrap();
|
||||
|
||||
assert!(
|
||||
native.contains("transaction_database.begin().await"),
|
||||
"{native}"
|
||||
);
|
||||
assert!(native.contains("transaction.commit().await"), "{native}");
|
||||
assert!(native.contains("transaction.rollback().await"), "{native}");
|
||||
assert!(native.contains("database::Executor::new"), "{native}");
|
||||
assert!(
|
||||
native.contains("postgres: \"INSERT INTO \\\"user\\\""),
|
||||
"{native}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn database_effects_flow_through_helpers_and_nested_transactions() {
|
||||
let helper = r#"
|
||||
create_user() (user: user.model.user.User) {
|
||||
user = user.create({
|
||||
email = "reader@example.com"
|
||||
display_name = "Reader"
|
||||
})
|
||||
}
|
||||
|
||||
public transaction register() (user: user.model.user.User) {
|
||||
user = create_user()
|
||||
}
|
||||
"#;
|
||||
let helper_sources = sources(helper);
|
||||
let helper_program = dever_core::check(&helper_sources).unwrap();
|
||||
let helper_native = dever_core::native::emit(
|
||||
&helper_program,
|
||||
&helper_sources,
|
||||
"user.service.account.register",
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
helper_native
|
||||
.matches("Option<&dever_runtime::database::Transaction>")
|
||||
.count()
|
||||
>= 2,
|
||||
"{helper_native}"
|
||||
);
|
||||
assert!(helper_native.contains("(None).await"), "{helper_native}");
|
||||
assert!(
|
||||
helper_native.contains("(_database).await"),
|
||||
"{helper_native}"
|
||||
);
|
||||
|
||||
let nested = helper.replacen(
|
||||
"create_user() (user: user.model.user.User)",
|
||||
"transaction create_user() (user: user.model.user.User)",
|
||||
1,
|
||||
);
|
||||
let nested_sources = sources(&nested);
|
||||
let nested_program = dever_core::check(&nested_sources).unwrap();
|
||||
let nested_native = dever_core::native::emit(
|
||||
&nested_program,
|
||||
&nested_sources,
|
||||
"user.service.account.register",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
nested_native
|
||||
.matches("transaction_database.begin().await")
|
||||
.count(),
|
||||
2,
|
||||
"{nested_native}"
|
||||
);
|
||||
assert!(
|
||||
nested_native.contains("(_database).await"),
|
||||
"{nested_native}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transaction_functions_require_database_work_and_reject_concurrency() {
|
||||
let mut empty = SourceMap::default();
|
||||
empty.add("main.dever", "public transaction save() () {}");
|
||||
let empty_error = dever_core::check(&empty)
|
||||
.unwrap_err()
|
||||
.iter()
|
||||
.map(|error| error.render(&empty))
|
||||
.collect::<String>();
|
||||
assert!(
|
||||
empty_error.contains("must perform a database operation"),
|
||||
"{empty_error}"
|
||||
);
|
||||
|
||||
let concurrent = r#"work() () {}
|
||||
public transaction register() (user: user.model.user.User) {
|
||||
task = run(work())
|
||||
user = user.create({
|
||||
email = "reader@example.com"
|
||||
display_name = "Reader"
|
||||
})
|
||||
wait(task)
|
||||
}
|
||||
"#;
|
||||
assert!(errors(concurrent).contains("transaction functions cannot start concurrent work"));
|
||||
|
||||
let streaming = r#"public transaction scan() () {
|
||||
users = user.stream({ size = 1 })
|
||||
close(users)
|
||||
}
|
||||
"#;
|
||||
assert!(errors(streaming).contains("transaction functions cannot start concurrent work"));
|
||||
|
||||
let structured_blocking = r#"hash(password: Secret) (value: Text) {
|
||||
value = blocking(dever.crypto.password_hash(password))
|
||||
}
|
||||
|
||||
public transaction register(password: Secret) (user: user.model.user.User) {
|
||||
password_hash = hash(password)
|
||||
user = user.create({
|
||||
email = password_hash
|
||||
display_name = "Reader"
|
||||
})
|
||||
}
|
||||
"#;
|
||||
assert!(
|
||||
errors(structured_blocking)
|
||||
.contains("transaction functions cannot perform password hashing or verification")
|
||||
);
|
||||
|
||||
let short_transaction = r#"hash(password: Secret) (value: Text) {
|
||||
value = blocking(dever.crypto.password_hash(password))
|
||||
}
|
||||
|
||||
transaction create_user(email: Text) (user: user.model.user.User) {
|
||||
user = user.create({
|
||||
email = email
|
||||
display_name = "Reader"
|
||||
})
|
||||
}
|
||||
|
||||
public register(password: Secret) (user: user.model.user.User) {
|
||||
password_hash = hash(password)
|
||||
user = create_user(password_hash)
|
||||
}
|
||||
"#;
|
||||
let sources = sources(short_transaction);
|
||||
dever_core::check(&sources).unwrap_or_else(|errors| {
|
||||
panic!(
|
||||
"{}",
|
||||
errors
|
||||
.iter()
|
||||
.map(|error| error.render(&sources))
|
||||
.collect::<String>()
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nullable_comparisons_use_null_safe_database_operators() {
|
||||
let service = r#"
|
||||
find_alias(value: Text?) (users: user.model.user.Page) {
|
||||
users = user.list({ where = alias == value })
|
||||
}
|
||||
|
||||
public main() (users: user.model.user.Page) {
|
||||
users = find_alias(null)
|
||||
}
|
||||
"#;
|
||||
let sources = sources(service);
|
||||
let program = dever_core::check(&sources).unwrap();
|
||||
let native = dever_core::native::emit(&program, &sources, "user.service.account.main").unwrap();
|
||||
|
||||
assert!(native.contains("\\\"alias\\\" IS ?1"), "{native}");
|
||||
assert!(
|
||||
native.contains("\\\"alias\\\" IS NOT DISTINCT FROM $1"),
|
||||
"{native}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn database_effects_follow_bound_handlers_inside_transactions() {
|
||||
let service = r#"
|
||||
apply(route: handler() (user: user.model.user.User)) (user: user.model.user.User) {
|
||||
user = route()
|
||||
}
|
||||
|
||||
forward(route: handler() (user: user.model.user.User)) (user: user.model.user.User) {
|
||||
user = apply(route)
|
||||
}
|
||||
|
||||
create_user() (user: user.model.user.User) {
|
||||
user = user.create({
|
||||
email = "reader@example.com"
|
||||
display_name = "Reader"
|
||||
})
|
||||
}
|
||||
|
||||
public transaction register() (user: user.model.user.User) {
|
||||
user = forward(create_user)
|
||||
}
|
||||
"#;
|
||||
let sources = sources(service);
|
||||
let program = dever_core::check(&sources).unwrap();
|
||||
let native =
|
||||
dever_core::native::emit(&program, &sources, "user.service.account.register").unwrap();
|
||||
|
||||
assert!(
|
||||
native.contains("transaction_database.begin().await"),
|
||||
"{native}"
|
||||
);
|
||||
assert!(native.contains("(_database).await"), "{native}");
|
||||
|
||||
let concurrent = service.replace(
|
||||
"create_user() (user: user.model.user.User) {",
|
||||
"work() () {}\n\ncreate_user() (user: user.model.user.User) {\n task = run(work())\n wait(task)",
|
||||
);
|
||||
assert!(errors(&concurrent).contains("transaction functions cannot start concurrent work"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_models_generate_scoped_storage_and_controlled_tenant_migration() {
|
||||
let mut sources = SourceMap::default();
|
||||
sources.add(
|
||||
"platform/role/model.dever",
|
||||
"global type Role { name: Text(1, 32) }",
|
||||
);
|
||||
sources.add(
|
||||
"user/profile/model.dever",
|
||||
"type Profile {\n role_id: platform.role.model.id\n display_name: Text(1, 64)\n}",
|
||||
);
|
||||
sources.add(
|
||||
"user/profile/app.dever",
|
||||
"count() (value: Int) { value = model.count() }",
|
||||
);
|
||||
sources.add("user/profile/api.dever", "cmd count = app.count");
|
||||
|
||||
let program = dever_core::check(&sources).unwrap();
|
||||
let native = dever_core::native::emit_project(
|
||||
&program,
|
||||
&sources,
|
||||
RuntimeProfile {
|
||||
sqlite: true,
|
||||
postgres: false,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(!native.contains("MODEL_DATABASE_"), "{native}");
|
||||
assert!(native.contains("MODEL_STORAGE_"), "{native}");
|
||||
assert!(
|
||||
native.contains("OnceLock<dever_runtime::database::StorageBinding>"),
|
||||
"{native}"
|
||||
);
|
||||
assert!(
|
||||
native.contains("dever_runtime::database::ModelScope::Global"),
|
||||
"{native}"
|
||||
);
|
||||
assert!(
|
||||
native.contains("dever_runtime::database::ModelScope::Tenant"),
|
||||
"{native}"
|
||||
);
|
||||
assert!(
|
||||
native.contains("dever_runtime::config::bootstrap_scoped"),
|
||||
"{native}"
|
||||
);
|
||||
assert!(
|
||||
native.contains("async fn tenant_migrate(tenant_id: i64)"),
|
||||
"{native}"
|
||||
);
|
||||
assert!(native.contains("--dever-tenant-migrate"), "{native}");
|
||||
assert!(
|
||||
native.contains("dever_runtime::tenant::begin_migration"),
|
||||
"{native}"
|
||||
);
|
||||
assert!(
|
||||
native.contains("dever_runtime::tenant::mark_database_ready"),
|
||||
"{native}"
|
||||
);
|
||||
assert!(
|
||||
native.contains("dever_runtime::tenant::finish_migration"),
|
||||
"{native}"
|
||||
);
|
||||
assert!(!native.contains("REFERENCES \\\"role\\\""), "{native}");
|
||||
}
|
||||
Reference in new issue
Block a user