use super::*;
pub(crate) fn emit_init_project_scaffold() {
let dir = env::var("UDB_INIT_DIR").unwrap_or_else(|_| ".".to_string());
for (rel_path, content) in scaffold_files() {
let path = format!("{dir}/{rel_path}");
let parent = std::path::Path::new(&path).parent().unwrap();
if let Err(e) = fs::create_dir_all(parent) {
eprintln!("could not create directory {}: {e}", parent.display());
continue;
}
if std::path::Path::new(&path).exists() {
eprintln!("skipping {path} (already exists)");
continue;
}
match fs::write(&path, content) {
Ok(()) => eprintln!("created {path}"),
Err(e) => eprintln!("failed to write {path}: {e}"),
}
}
eprintln!("\nProject scaffold created.");
eprintln!("{}", migration_to_orm_next_steps());
}
pub(crate) fn migration_to_orm_next_steps() -> String {
[
"Next steps (migration → ORM models):",
" 1. udb plan # preview the migration plan from your proto",
" 2. udb sync-migrations # write db_ops/migrations artifacts (proto is source of truth)",
" 3. udb system-ddl | psql $DATABASE_URL # apply the schema to the database",
" 4. udb orm scaffold --lang <lang> [--entity <pkg.Message>]",
" # generate typed entity/repository models (reuses `udb sdk generate`)",
"See docs/orm-scaffold.md for the full migrate-plan/apply → model-generation workflow.",
]
.join("\n")
}
pub(crate) fn scaffold_files() -> Vec<(&'static str, &'static str)> {
let proto_sample = r#"syntax = "proto3";
package myapp.v1;
import "udb/core/common/v1/db.proto";
import "udb/core/common/v1/security.proto";
message User {
option (udb.core.common.v1.pg_table) = {
table_name: "users"
schema_name: "app"
is_table: true
enable_rls: true
};
option (udb.core.common.v1.db_table_security) = {
tenant_column: "tenant_id"
tenant_isolation_mode: "tenant"
};
string id = 1 [(udb.core.common.v1.pg_column) = {
column_name: "id"
sql_type: "UUID"
primary_key: true
not_null: true
}];
string tenant_id = 2 [(udb.core.common.v1.pg_column) = {
column_name: "tenant_id"
sql_type: "UUID"
not_null: true
}];
string email = 3 [
(udb.core.common.v1.pg_column) = {
column_name: "email"
sql_type: "TEXT"
encrypted: true
},
(udb.core.common.v1.pii) = true,
(udb.core.common.v1.log_masked) = true,
(udb.core.common.v1.data_purpose) = "login"
];
string created_at = 4 [(udb.core.common.v1.pg_column) = {
column_name: "created_at"
sql_type: "TIMESTAMPTZ"
not_null: true
default_value: "now()"
}];
}
"#;
let config_template = r#"# configs/database.yaml — UDB runtime configuration template
# Copy and customize this file. Expand env-vars with ${VAR} syntax.
tier1_postgres:
primary:
dsn: "${DATABASE_URL}"
max_connections: 50
tier2_redis:
session:
dsn: "${REDIS_URL}"
tier3_qdrant:
embeddings:
url: "${QDRANT_URL}"
tier4_minio:
artifacts:
endpoint: "${S3_ENDPOINT}"
access_key: "${AWS_ACCESS_KEY_ID}"
secret_key: "${AWS_SECRET_ACCESS_KEY}"
region: "us-east-1"
"#;
let docker_compose = r#"# docker-compose.udb.yml — Local UDB development environment
version: "3.8"
services:
postgres:
image: postgres:16
environment:
POSTGRES_USER: udb
POSTGRES_PASSWORD: udb
POSTGRES_DB: udb
ports: ["5432:5432"]
redis:
image: redis:7
ports: ["6379:6379"]
qdrant:
image: qdrant/qdrant:latest
ports: ["6333:6333"]
minio:
image: minio/minio:latest
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: minioadmin
ports: ["9000:9000", "9001:9001"]
kafka:
image: confluentinc/cp-kafka:7.6.0
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
ports: ["9092:9092"]
depends_on: [zookeeper]
zookeeper:
image: confluentinc/cp-zookeeper:7.6.0
environment:
ZOOKEEPER_CLIENT_PORT: 2181
"#;
let go_client = r#"// examples/go/client.go — minimal UDB gRPC client (Go)
// go get google.golang.org/grpc github.com/fahara02/udb/sdk/go/gen/udb/entity/v1 github.com/fahara02/udb/sdk/go/gen/udb/services/v1
package main
import (
"context"
"encoding/json"
"fmt"
"log"
entityv1 "github.com/fahara02/udb/sdk/go/gen/udb/entity/v1"
servicesv1 "github.com/fahara02/udb/sdk/go/gen/udb/services/v1"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func main() {
conn, err := grpc.NewClient("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("dial: %v", err)
}
defer conn.Close()
c := servicesv1.NewDataBrokerClient(conn)
resp, err := c.GetHealthReport(context.Background(), &entityv1.HealthReportRequest{
Context: &entityv1.RequestContext{Purpose: "health", ServiceIdentity: "example"},
WithProbes: false,
})
if err != nil {
log.Fatalf("GetHealthReport: %v", err)
}
b, _ := json.MarshalIndent(resp, "", " ")
fmt.Println(string(b))
}
"#;
let python_client = r#"# examples/python/client.py — minimal UDB gRPC client (Python)
# pip install grpcio grpcio-tools
import grpc, json, sys
sys.path.insert(0, "gen/python")
from google.protobuf.json_format import MessageToDict
from udb.entity.v1 import types_pb2
from udb.services.v1 import data_broker_pb2_grpc
def main():
channel = grpc.insecure_channel("localhost:50051")
stub = data_broker_pb2_grpc.DataBrokerStub(channel)
resp = stub.GetHealthReport(types_pb2.HealthReportRequest(
context=types_pb2.RequestContext(purpose="health", service_identity="example"),
with_probes=False,
))
print(json.dumps(MessageToDict(resp), indent=2))
if __name__ == "__main__":
main()
"#;
let typescript_client = r#"// examples/typescript/client.ts — minimal UDB gRPC client (TypeScript)
// npm install @grpc/grpc-js @grpc/proto-loader
import * as grpc from "@grpc/grpc-js";
import * as protoLoader from "@grpc/proto-loader";
import path from "path";
const PROTO_PATH = path.resolve(__dirname, "../../proto/udb/services/v1/data_broker.proto");
const def = protoLoader.loadSync(PROTO_PATH, { keepCase: true, longs: String });
const udbProto = grpc.loadPackageDefinition(def) as any;
const client = new udbProto.udb.services.v1.DataBroker(
"localhost:50051",
grpc.credentials.createInsecure()
);
client.GetHealthReport(
{ context: { purpose: "health", service_identity: "example" }, with_probes: false },
(err: Error | null, resp: unknown) => {
if (err) { console.error(err); process.exit(1); }
console.log(JSON.stringify(resp, null, 2));
}
);
"#;
let csharp_client = r#"// examples/csharp/Client.cs — minimal UDB gRPC client (C#)
// dotnet add package Grpc.Net.Client Google.Protobuf Grpc.Tools
using Grpc.Net.Client;
using Udb.Entity.V1;
using Udb.Services.V1;
using var channel = GrpcChannel.ForAddress("http://localhost:50051");
var client = new DataBroker.DataBrokerClient(channel);
var resp = await client.GetHealthReportAsync(new HealthReportRequest {
Context = new RequestContext { Purpose = "health", ServiceIdentity = "example" },
WithProbes = false
});
Console.WriteLine(resp);
"#;
let java_client = r#"// examples/java/Client.java — minimal UDB gRPC client (Java)
// mvn dependency: dev.udb:udb-java-client
import com.udb.entity.v1.HealthReportRequest;
import com.udb.entity.v1.RequestContext;
import com.udb.services.v1.DataBrokerGrpc;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
public final class Client {
private Client() {}
public static void main(String[] args) {
ManagedChannel channel = ManagedChannelBuilder
.forAddress("localhost", 50051)
.usePlaintext()
.build();
try {
DataBrokerGrpc.DataBrokerBlockingStub client = DataBrokerGrpc.newBlockingStub(channel);
HealthReportRequest req = HealthReportRequest.newBuilder()
.setContext(RequestContext.newBuilder()
.setPurpose("health")
.setServiceIdentity("example")
.build())
.setWithProbes(false)
.build();
System.out.println(client.getHealthReport(req));
} finally {
channel.shutdownNow();
}
}
}
"#;
let php_client = r#"<?php
// examples/php/client.php — minimal UDB gRPC client (PHP)
// composer require fahara02/udb-laravel
require __DIR__ . '/vendor/autoload.php';
use Grpc\ChannelCredentials;
use Udb\Entity\V1\HealthReportRequest;
use Udb\Entity\V1\RequestContext;
use Udb\Services\V1\DataBrokerClient;
$client = new DataBrokerClient('localhost:50051', [
'credentials' => ChannelCredentials::createInsecure(),
]);
$ctx = (new RequestContext())
->setPurpose('health')
->setServiceIdentity('example');
$req = (new HealthReportRequest())
->setContext($ctx)
->setWithProbes(false);
[$resp, $status] = $client->GetHealthReport($req)->wait();
if ($status->code !== \Grpc\STATUS_OK) {
fwrite(STDERR, "GetHealthReport failed: {$status->details}\n");
exit(1);
}
echo $resp->serializeToJsonString(), PHP_EOL;
"#;
vec![
("proto/app/v1/user.proto", proto_sample),
("configs/database.yaml", config_template),
("docker-compose.udb.yml", docker_compose),
("examples/go/client.go", go_client),
("examples/python/client.py", python_client),
("examples/typescript/client.ts", typescript_client),
("examples/csharp/Client.cs", csharp_client),
("examples/java/Client.java", java_client),
("examples/php/client.php", php_client),
]
}
#[cfg(test)]
mod tests {
use super::*;
fn file(rel: &str) -> &'static str {
scaffold_files()
.into_iter()
.find(|(p, _)| *p == rel)
.unwrap_or_else(|| panic!("scaffold missing {rel}"))
.1
}
#[test]
fn go_example_uses_the_real_module_path_and_grpc_newclient() {
let go = file("examples/go/client.go");
assert!(
go.contains("github.com/fahara02/udb/sdk/go/gen/udb/entity/v1"),
"Go example must import the real entity gen path"
);
assert!(
go.contains("github.com/fahara02/udb/sdk/go/gen/udb/services/v1"),
"Go example must import the real services gen path"
);
assert!(
!go.contains("github.com/udb-project/"),
"Go example still references the fictional udb-project org"
);
assert!(
go.contains("grpc.NewClient("),
"Go example must use grpc.NewClient"
);
assert!(
!go.contains("grpc.Dial("),
"Go example must not use the deprecated grpc.Dial"
);
}
#[test]
fn typescript_example_loads_the_data_broker_proto() {
let ts = file("examples/typescript/client.ts");
assert!(
ts.contains("@grpc/grpc-js"),
"TS example must import @grpc/grpc-js"
);
assert!(
ts.contains("data_broker.proto"),
"TS example must load the DataBroker proto"
);
}
#[test]
fn scaffold_emits_examples_for_all_six_sdks() {
let files = scaffold_files()
.into_iter()
.map(|(path, _)| path)
.collect::<std::collections::BTreeSet<_>>();
for expected in [
"examples/go/client.go",
"examples/python/client.py",
"examples/typescript/client.ts",
"examples/csharp/Client.cs",
"examples/java/Client.java",
"examples/php/client.php",
] {
assert!(files.contains(expected), "scaffold missing {expected}");
}
}
#[test]
fn scaffold_proto_catalogs_with_default_parser_options() {
let root = std::env::temp_dir().join(format!(
"udb_scaffold_quickstart_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system clock should be after epoch")
.as_nanos()
));
for (rel, content) in scaffold_files()
.into_iter()
.filter(|(rel, _)| rel.ends_with(".proto"))
{
let path = root.join(rel);
std::fs::create_dir_all(path.parent().expect("scaffold proto has parent"))
.expect("create scaffold proto parent");
std::fs::write(&path, content).expect("write scaffold proto");
}
let schemas = udb::parse_directory(root.join("proto"), &udb::ParserConfig::default())
.expect("default parser should read scaffold annotations");
let _ = std::fs::remove_dir_all(&root);
let user = schemas
.iter()
.find(|schema| schema.message_name == "User")
.expect("scaffold should yield a User schema");
assert_eq!(user.table_name, "users");
assert_eq!(user.schema_name, "app");
assert!(user.is_table);
assert_eq!(user.columns.len(), 4);
assert!(
user.columns
.iter()
.any(|column| column.column_name == "email" && column.security.is_pii),
"scaffold email field should retain scalar security metadata"
);
}
}