649 lines
25 KiB
JavaScript
649 lines
25 KiB
JavaScript
/**
|
|
* Shared Model Contract Layer
|
|
*
|
|
* 单一字段定义源(Single Source of Truth)。
|
|
* 所有 agent 通过 import { createContract } from "./model-contract.mjs" 引用,
|
|
* 禁止自行定义任何字段名。
|
|
*
|
|
* 提供:
|
|
* - entities[] — 所有实体的字段定义(PascalCase 名 + snake_case 表名)
|
|
* - auth{} — 认证配置
|
|
* - permissions[] — RBAC 权限矩阵
|
|
* - fieldMapping{} — snake_case ↔ camelCase 双向映射
|
|
* - validation{} — 字段级校验规则
|
|
*/
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
// 1. Domain-Specific Entity Templates
|
|
// ═══════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* Base entity templates for common domains.
|
|
* These are the canonical field definitions for every entity.
|
|
* Key: domain name → Array of entity templates.
|
|
*/
|
|
const DOMAIN_ENTITIES = {
|
|
enterprise: [
|
|
{
|
|
name: "User",
|
|
table: "users",
|
|
description: "系统用户",
|
|
fields: [
|
|
{ name: "id", type: "UUID", isPrimary: true, isAuto: true },
|
|
{ name: "username", type: "VARCHAR(128)", required: true, unique: true, validation: { minLength: 2, maxLength: 50 } },
|
|
{ name: "password_hash", type: "VARCHAR(256)", required: true, isSecret: true },
|
|
{ name: "nickname", type: "VARCHAR(128)" },
|
|
{ name: "role", type: "VARCHAR(32)", defaultValue: "user", enum: ["user", "admin"] },
|
|
{ name: "phone", type: "VARCHAR(20)" },
|
|
{ name: "avatar_url", type: "TEXT" },
|
|
{ name: "created_at", type: "TIMESTAMP", isAuto: true },
|
|
{ name: "updated_at", type: "TIMESTAMP", isAuto: true },
|
|
],
|
|
relationships: [
|
|
{ type: "hasMany", entity: "contracts", via: "user_id" },
|
|
{ type: "hasMany", entity: "customers", via: "user_id" },
|
|
],
|
|
},
|
|
{
|
|
name: "Contract",
|
|
table: "contracts",
|
|
description: "合同",
|
|
fields: [
|
|
{ name: "id", type: "UUID", isPrimary: true, isAuto: true },
|
|
{ name: "user_id", type: "UUID", required: true, fkEntity: "users", fkColumn: "id" },
|
|
{ name: "title", type: "VARCHAR(256)", required: true },
|
|
{ name: "party_a", type: "VARCHAR(128)" },
|
|
{ name: "party_b", type: "VARCHAR(128)" },
|
|
{ name: "amount", type: "DECIMAL(14,2)" },
|
|
{ name: "signed_at", type: "DATE" },
|
|
{ name: "expires_at", type: "DATE" },
|
|
{ name: "status", type: "VARCHAR(32)", defaultValue: "draft", enum: ["draft", "pending", "active", "expired", "terminated"] },
|
|
{ name: "file_url", type: "TEXT" },
|
|
{ name: "created_at", type: "TIMESTAMP", isAuto: true },
|
|
{ name: "updated_at", type: "TIMESTAMP", isAuto: true },
|
|
],
|
|
relationships: [
|
|
{ type: "belongsTo", entity: "users", via: "user_id" },
|
|
],
|
|
},
|
|
{
|
|
name: "Approval",
|
|
table: "approvals",
|
|
description: "审批流程",
|
|
fields: [
|
|
{ name: "id", type: "UUID", isPrimary: true, isAuto: true },
|
|
{ name: "user_id", type: "UUID", required: true, fkEntity: "users", fkColumn: "id" },
|
|
{ name: "entity_type", type: "VARCHAR(32)", required: true },
|
|
{ name: "entity_id", type: "UUID" },
|
|
{ name: "applicant_id", type: "UUID", required: true, fkEntity: "users", fkColumn: "id" },
|
|
{ name: "status", type: "VARCHAR(32)", defaultValue: "pending", enum: ["pending", "approved", "rejected"] },
|
|
{ name: "form_data", type: "JSONB" },
|
|
{ name: "created_at", type: "TIMESTAMP", isAuto: true },
|
|
{ name: "updated_at", type: "TIMESTAMP", isAuto: true },
|
|
],
|
|
relationships: [
|
|
{ type: "belongsTo", entity: "users", via: "user_id" },
|
|
{ type: "belongsTo", entity: "users", via: "applicant_id", as: "applicant" },
|
|
],
|
|
},
|
|
{
|
|
name: "Customer",
|
|
table: "customers",
|
|
description: "客户",
|
|
fields: [
|
|
{ name: "id", type: "UUID", isPrimary: true, isAuto: true },
|
|
{ name: "user_id", type: "UUID", required: true, fkEntity: "users", fkColumn: "id" },
|
|
{ name: "name", type: "VARCHAR(128)", required: true },
|
|
{ name: "email", type: "VARCHAR(256)" },
|
|
{ name: "phone", type: "VARCHAR(20)" },
|
|
{ name: "company", type: "VARCHAR(128)" },
|
|
{ name: "source", type: "VARCHAR(64)" },
|
|
{ name: "tags", type: "JSONB", defaultValue: "[]" },
|
|
{ name: "created_at", type: "TIMESTAMP", isAuto: true },
|
|
{ name: "updated_at", type: "TIMESTAMP", isAuto: true },
|
|
],
|
|
relationships: [
|
|
{ type: "belongsTo", entity: "users", via: "user_id" },
|
|
],
|
|
},
|
|
{
|
|
name: "Reminder",
|
|
table: "reminders",
|
|
description: "到期提醒",
|
|
fields: [
|
|
{ name: "id", type: "UUID", isPrimary: true, isAuto: true },
|
|
{ name: "user_id", type: "UUID", required: true, fkEntity: "users", fkColumn: "id" },
|
|
{ name: "entity_type", type: "VARCHAR(32)", required: true },
|
|
{ name: "entity_id", type: "UUID" },
|
|
{ name: "remind_at", type: "TIMESTAMP", required: true },
|
|
{ name: "message", type: "TEXT" },
|
|
{ name: "sent", type: "BOOLEAN", defaultValue: "false" },
|
|
{ name: "created_at", type: "TIMESTAMP", isAuto: true },
|
|
{ name: "updated_at", type: "TIMESTAMP", isAuto: true },
|
|
],
|
|
relationships: [
|
|
{ type: "belongsTo", entity: "users", via: "user_id" },
|
|
],
|
|
},
|
|
],
|
|
|
|
pet: [
|
|
{
|
|
name: "User",
|
|
table: "users",
|
|
description: "系统用户",
|
|
fields: [
|
|
{ name: "id", type: "UUID", isPrimary: true, isAuto: true },
|
|
{ name: "username", type: "VARCHAR(128)", required: true, unique: true, validation: { minLength: 2, maxLength: 50 } },
|
|
{ name: "password_hash", type: "VARCHAR(256)", required: true, isSecret: true },
|
|
{ name: "nickname", type: "VARCHAR(128)" },
|
|
{ name: "role", type: "VARCHAR(32)", defaultValue: "user", enum: ["user", "admin"] },
|
|
{ name: "phone", type: "VARCHAR(20)" },
|
|
{ name: "avatar_url", type: "TEXT" },
|
|
{ name: "created_at", type: "TIMESTAMP", isAuto: true },
|
|
{ name: "updated_at", type: "TIMESTAMP", isAuto: true },
|
|
],
|
|
relationships: [],
|
|
},
|
|
{
|
|
name: "Pet",
|
|
table: "pets",
|
|
description: "宠物",
|
|
fields: [
|
|
{ name: "id", type: "UUID", isPrimary: true, isAuto: true },
|
|
{ name: "user_id", type: "UUID", required: true, fkEntity: "users", fkColumn: "id" },
|
|
{ name: "name", type: "VARCHAR(64)", required: true },
|
|
{ name: "species", type: "VARCHAR(32)" },
|
|
{ name: "breed", type: "VARCHAR(64)" },
|
|
{ name: "birth_date", type: "DATE" },
|
|
{ name: "weight_kg", type: "DECIMAL(5,2)" },
|
|
{ name: "avatar_url", type: "TEXT" },
|
|
{ name: "created_at", type: "TIMESTAMP", isAuto: true },
|
|
{ name: "updated_at", type: "TIMESTAMP", isAuto: true },
|
|
],
|
|
relationships: [
|
|
{ type: "belongsTo", entity: "users", via: "user_id" },
|
|
],
|
|
},
|
|
],
|
|
|
|
ecommerce: [
|
|
{
|
|
name: "User",
|
|
table: "users",
|
|
description: "系统用户",
|
|
fields: [
|
|
{ name: "id", type: "UUID", isPrimary: true, isAuto: true },
|
|
{ name: "username", type: "VARCHAR(128)", required: true, unique: true, validation: { minLength: 2, maxLength: 50 } },
|
|
{ name: "password_hash", type: "VARCHAR(256)", required: true, isSecret: true },
|
|
{ name: "nickname", type: "VARCHAR(128)" },
|
|
{ name: "role", type: "VARCHAR(32)", defaultValue: "user", enum: ["user", "admin"] },
|
|
{ name: "phone", type: "VARCHAR(20)" },
|
|
{ name: "avatar_url", type: "TEXT" },
|
|
{ name: "created_at", type: "TIMESTAMP", isAuto: true },
|
|
{ name: "updated_at", type: "TIMESTAMP", isAuto: true },
|
|
],
|
|
relationships: [],
|
|
},
|
|
{
|
|
name: "Product",
|
|
table: "products",
|
|
description: "商品",
|
|
fields: [
|
|
{ name: "id", type: "UUID", isPrimary: true, isAuto: true },
|
|
{ name: "user_id", type: "UUID", required: true, fkEntity: "users", fkColumn: "id" },
|
|
{ name: "title", type: "VARCHAR(256)", required: true },
|
|
{ name: "description", type: "TEXT" },
|
|
{ name: "price", type: "DECIMAL(10,2)" },
|
|
{ name: "stock", type: "INTEGER", defaultValue: "0" },
|
|
{ name: "images", type: "JSONB", defaultValue: "[]" },
|
|
{ name: "created_at", type: "TIMESTAMP", isAuto: true },
|
|
{ name: "updated_at", type: "TIMESTAMP", isAuto: true },
|
|
],
|
|
relationships: [
|
|
{ type: "belongsTo", entity: "users", via: "user_id" },
|
|
],
|
|
},
|
|
{
|
|
name: "Order",
|
|
table: "orders",
|
|
description: "订单",
|
|
fields: [
|
|
{ name: "id", type: "UUID", isPrimary: true, isAuto: true },
|
|
{ name: "user_id", type: "UUID", required: true, fkEntity: "users", fkColumn: "id" },
|
|
{ name: "status", type: "VARCHAR(32)", defaultValue: "pending", enum: ["pending", "paid", "shipped", "delivered", "cancelled"] },
|
|
{ name: "total_amount", type: "DECIMAL(12,2)" },
|
|
{ name: "address_id", type: "UUID", fkEntity: "addresses", fkColumn: "id" },
|
|
{ name: "created_at", type: "TIMESTAMP", isAuto: true },
|
|
{ name: "updated_at", type: "TIMESTAMP", isAuto: true },
|
|
],
|
|
relationships: [
|
|
{ type: "belongsTo", entity: "users", via: "user_id" },
|
|
],
|
|
},
|
|
],
|
|
|
|
education: [
|
|
{
|
|
name: "User",
|
|
table: "users",
|
|
description: "系统用户",
|
|
fields: [
|
|
{ name: "id", type: "UUID", isPrimary: true, isAuto: true },
|
|
{ name: "username", type: "VARCHAR(128)", required: true, unique: true, validation: { minLength: 2, maxLength: 50 } },
|
|
{ name: "password_hash", type: "VARCHAR(256)", required: true, isSecret: true },
|
|
{ name: "nickname", type: "VARCHAR(128)" },
|
|
{ name: "role", type: "VARCHAR(32)", defaultValue: "user", enum: ["user", "admin", "instructor"] },
|
|
{ name: "phone", type: "VARCHAR(20)" },
|
|
{ name: "avatar_url", type: "TEXT" },
|
|
{ name: "created_at", type: "TIMESTAMP", isAuto: true },
|
|
{ name: "updated_at", type: "TIMESTAMP", isAuto: true },
|
|
],
|
|
relationships: [],
|
|
},
|
|
],
|
|
|
|
note: [
|
|
{
|
|
name: "User",
|
|
table: "users",
|
|
description: "系统用户",
|
|
fields: [
|
|
{ name: "id", type: "UUID", isPrimary: true, isAuto: true },
|
|
{ name: "username", type: "VARCHAR(128)", required: true, unique: true, validation: { minLength: 2, maxLength: 50 } },
|
|
{ name: "password_hash", type: "VARCHAR(256)", required: true, isSecret: true },
|
|
{ name: "nickname", type: "VARCHAR(128)" },
|
|
{ name: "role", type: "VARCHAR(32)", defaultValue: "user", enum: ["user", "admin"] },
|
|
{ name: "phone", type: "VARCHAR(20)" },
|
|
{ name: "avatar_url", type: "TEXT" },
|
|
{ name: "created_at", type: "TIMESTAMP", isAuto: true },
|
|
{ name: "updated_at", type: "TIMESTAMP", isAuto: true },
|
|
],
|
|
relationships: [],
|
|
},
|
|
],
|
|
|
|
fitness: [
|
|
{
|
|
name: "User",
|
|
table: "users",
|
|
description: "系统用户",
|
|
fields: [
|
|
{ name: "id", type: "UUID", isPrimary: true, isAuto: true },
|
|
{ name: "username", type: "VARCHAR(128)", required: true, unique: true, validation: { minLength: 2, maxLength: 50 } },
|
|
{ name: "password_hash", type: "VARCHAR(256)", required: true, isSecret: true },
|
|
{ name: "nickname", type: "VARCHAR(128)" },
|
|
{ name: "role", type: "VARCHAR(32)", defaultValue: "user", enum: ["user", "admin"] },
|
|
{ name: "phone", type: "VARCHAR(20)" },
|
|
{ name: "avatar_url", type: "TEXT" },
|
|
{ name: "created_at", type: "TIMESTAMP", isAuto: true },
|
|
{ name: "updated_at", type: "TIMESTAMP", isAuto: true },
|
|
],
|
|
relationships: [],
|
|
},
|
|
],
|
|
};
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
// 2. Field Mapping Engine
|
|
// ═══════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* Build bidirectional snake_case ↔ camelCase mappings from all contract entities.
|
|
* Scanned once; cached in the returned contract.
|
|
*/
|
|
function buildFieldMappings(entities) {
|
|
const snakeToCamel = {};
|
|
const camelToSnake = {};
|
|
|
|
// Base conversions from common SQL columns
|
|
const baseMappings = {
|
|
snakeToCamel: {
|
|
"created_at": "createdAt",
|
|
"updated_at": "updatedAt",
|
|
"user_id": "userId",
|
|
},
|
|
camelToSnake: {
|
|
"createdAt": "created_at",
|
|
"updatedAt": "updated_at",
|
|
"userId": "user_id",
|
|
},
|
|
};
|
|
|
|
Object.assign(snakeToCamel, baseMappings.snakeToCamel);
|
|
Object.assign(camelToSnake, baseMappings.camelToSnake);
|
|
|
|
for (const entity of entities) {
|
|
for (const field of entity.fields) {
|
|
const snake = field.name; // fields are stored in snake_case
|
|
// Convert to camelCase
|
|
const camel = snake.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
|
|
if (snake !== camel) {
|
|
snakeToCamel[snake] = camel;
|
|
camelToSnake[camel] = snake;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add common patterns
|
|
const known = [
|
|
["id", "id"],
|
|
["username", "username"],
|
|
["password_hash", "passwordHash"],
|
|
["nickname", "nickname"],
|
|
["role", "role"],
|
|
["phone", "phone"],
|
|
["avatar_url", "avatarUrl"],
|
|
["title", "title"],
|
|
["description", "description"],
|
|
["status", "status"],
|
|
["data", "data"],
|
|
["amount", "amount"],
|
|
["party_a", "partyA"],
|
|
["party_b", "partyB"],
|
|
["signed_at", "signedAt"],
|
|
["expires_at", "expiresAt"],
|
|
["file_url", "fileUrl"],
|
|
["entity_type", "entityType"],
|
|
["entity_id", "entityId"],
|
|
["applicant_id", "applicantId"],
|
|
["form_data", "formData"],
|
|
["name", "name"],
|
|
["email", "email"],
|
|
["company", "company"],
|
|
["source", "source"],
|
|
["tags", "tags"],
|
|
["breed", "breed"],
|
|
["species", "species"],
|
|
["birth_date", "birthDate"],
|
|
["weight_kg", "weightKg"],
|
|
["price", "price"],
|
|
["stock", "stock"],
|
|
["images", "images"],
|
|
["total_amount", "totalAmount"],
|
|
["address_id", "addressId"],
|
|
["remind_at", "remindAt"],
|
|
["message", "message"],
|
|
["sent", "sent"],
|
|
];
|
|
|
|
for (const [snake, camel] of known) {
|
|
if (!snakeToCamel[snake]) snakeToCamel[snake] = camel;
|
|
if (!camelToSnake[camel]) camelToSnake[camel] = snake;
|
|
}
|
|
|
|
return { snakeToCamel, camelToSnake };
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
// 3. Auth Configuration
|
|
// ═══════════════════════════════════════════════════════
|
|
|
|
function buildAuthConfig(domain) {
|
|
// Enterprise domain has stricter auth
|
|
const isEnterprise = domain === "enterprise";
|
|
|
|
return {
|
|
registrationFields: ["username", "password", "nickname"],
|
|
loginFields: ["username", "password"],
|
|
jwtPayload: ["userId", "username", "role"],
|
|
passwordPolicy: {
|
|
minLength: isEnterprise ? 8 : 6,
|
|
requireSpecial: isEnterprise,
|
|
},
|
|
};
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
// 4. Permissions Matrix
|
|
// ═══════════════════════════════════════════════════════
|
|
|
|
function buildPermissions(domain) {
|
|
return [
|
|
{ role: "admin", allow: ["*"] },
|
|
{ role: "user", allow: ["read:own", "create:*", "update:own", "delete:own"] },
|
|
];
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
// 5. Validation Rules
|
|
// ═══════════════════════════════════════════════════════
|
|
|
|
function buildValidation(entities) {
|
|
const rules = {};
|
|
|
|
for (const entity of entities) {
|
|
const fieldRules = {};
|
|
for (const field of entity.fields) {
|
|
if (field.validation) {
|
|
fieldRules[field.name] = { ...field.validation };
|
|
}
|
|
// Auto-generate from field properties
|
|
if (field.required || field.unique || field.enum || field.defaultValue !== undefined) {
|
|
if (!fieldRules[field.name]) fieldRules[field.name] = {};
|
|
}
|
|
if (field.required) {
|
|
fieldRules[field.name].required = true;
|
|
}
|
|
if (field.unique) {
|
|
fieldRules[field.name].unique = true;
|
|
}
|
|
if (field.enum) {
|
|
fieldRules[field.name].enum = field.enum;
|
|
}
|
|
if (field.defaultValue !== undefined) {
|
|
fieldRules[field.name].defaultValue = field.defaultValue;
|
|
}
|
|
}
|
|
if (Object.keys(fieldRules).length > 0) {
|
|
rules[entity.name] = fieldRules;
|
|
}
|
|
}
|
|
|
|
return rules;
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
// 6. Main Contract Builder
|
|
// ═══════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* Create a Model Contract from PRD and Architecture.
|
|
*
|
|
* The contract is the SINGLE SOURCE OF TRUTH for all entity field definitions.
|
|
* All generators MUST read from the contract; they MUST NOT invent field names.
|
|
*
|
|
* @param {object} prd — PRD from SF-01 (project-intake-agent)
|
|
* @param {object} arch — Architecture from SF-02 (architecture-agent), optional
|
|
* @returns {object} Contract object
|
|
*/
|
|
export function createContract(prd, arch) {
|
|
const domain = (prd && prd.domain) || (arch && arch.domain) || "generic";
|
|
const projectName = (prd && prd.projectName) || (arch && arch.projectName) || "App";
|
|
|
|
// ── Get domain-specific entities ──
|
|
let entities = DOMAIN_ENTITIES[domain] || DOMAIN_ENTITIES.enterprise;
|
|
|
|
// Deep clone to avoid mutation
|
|
entities = JSON.parse(JSON.stringify(entities));
|
|
|
|
// ── Augment with domain-specific entities from features/APIs ──
|
|
// If architecture has derived tables, merge them into the contract
|
|
if (arch && arch.databaseSchema) {
|
|
const existingTables = new Set(entities.map(e => e.table));
|
|
const existingNames = new Set(entities.map(e => e.name));
|
|
|
|
for (const table of arch.databaseSchema) {
|
|
if (!existingTables.has(table.table) && table.table !== "users") {
|
|
// Auto-generate PascalCase entity name from snake_case table
|
|
const pascalName = table.table
|
|
.split("_")
|
|
.map(w => w.charAt(0).toUpperCase() + w.slice(1))
|
|
.join("");
|
|
|
|
if (!existingNames.has(pascalName)) {
|
|
const entity = {
|
|
name: pascalName,
|
|
table: table.table,
|
|
description: table.description || `${pascalName}表`,
|
|
fields: (table.fields || []).map(f => ({
|
|
name: f.name,
|
|
type: f.type || "TEXT",
|
|
required: (f.constraints || "").includes("NOT NULL") || (f.constraints || "").includes("PK"),
|
|
isPrimary: (f.constraints || "").includes("PK"),
|
|
isAuto: f.name === "created_at" || f.name === "updated_at" || (f.constraints || "").includes("DEFAULT"),
|
|
...((f.constraints || "").includes("UNIQUE") ? { unique: true } : {}),
|
|
...((f.constraints || "").includes("FK") ? {
|
|
fkEntity: (f.name.endsWith("_id") ? f.name.replace(/_id$/, "s") : "items"),
|
|
fkColumn: "id",
|
|
} : {}),
|
|
})),
|
|
relationships: [],
|
|
};
|
|
|
|
// Run FK detection on fields
|
|
for (const field of entity.fields) {
|
|
if (field.fkEntity) {
|
|
entity.relationships.push({
|
|
type: "belongsTo",
|
|
entity: field.fkEntity,
|
|
via: field.name,
|
|
});
|
|
}
|
|
}
|
|
|
|
entities.push(entity);
|
|
existingTables.add(table.table);
|
|
existingNames.add(pascalName);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Build mappings ──
|
|
const fieldMapping = buildFieldMappings(entities);
|
|
const auth = buildAuthConfig(domain);
|
|
const permissions = buildPermissions(domain);
|
|
const validation = buildValidation(entities);
|
|
|
|
return {
|
|
projectName,
|
|
domain,
|
|
entities,
|
|
auth,
|
|
permissions,
|
|
fieldMapping,
|
|
validation,
|
|
};
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
// 7. Contract Utility Functions
|
|
// ═══════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* Look up a field in the contract by entity.table and field name (snake_case).
|
|
*
|
|
* @param {object} contract
|
|
* @param {string} tableName — snake_case table name
|
|
* @param {string} fieldName — snake_case field name
|
|
* @returns {object|null} Field definition
|
|
*/
|
|
export function lookupField(contract, tableName, fieldName) {
|
|
for (const entity of contract.entities) {
|
|
if (entity.table === tableName) {
|
|
return entity.fields.find(f => f.name === fieldName) || null;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get the camelCase name for a snake_case column from the contract's fieldMapping.
|
|
*
|
|
* @param {object} contract
|
|
* @param {string} snakeName
|
|
* @returns {string}
|
|
*/
|
|
export function toCamel(contract, snakeName) {
|
|
return contract.fieldMapping.snakeToCamel[snakeName] || snakeName;
|
|
}
|
|
|
|
/**
|
|
* Get the snake_case name for a camelCase property from the contract's fieldMapping.
|
|
*
|
|
* @param {object} contract
|
|
* @param {string} camelName
|
|
* @returns {string}
|
|
*/
|
|
export function toSnake(contract, camelName) {
|
|
return contract.fieldMapping.camelToSnake[camelName] || camelName;
|
|
}
|
|
|
|
/**
|
|
* Get the TypeScript type for a contract field.
|
|
*
|
|
* @param {object} field — Contract field definition
|
|
* @returns {string} TypeScript type string
|
|
*/
|
|
export function tsType(field) {
|
|
const t = (field.type || "").toUpperCase();
|
|
if (t.includes("INT") || t.includes("SERIAL") || t.includes("BIGINT") || t.includes("DECIMAL") || t.includes("NUMERIC") || t.includes("FLOAT") || t.includes("DOUBLE") || t.includes("REAL")) return "number";
|
|
if (t.includes("BOOL")) return "boolean";
|
|
if (t.includes("JSONB") || t.includes("JSON")) return "Record<string, unknown>";
|
|
return "string";
|
|
}
|
|
|
|
/**
|
|
* Get the SQLite column type for a contract field.
|
|
*
|
|
* @param {object} field — Contract field definition
|
|
* @returns {string} SQLite type
|
|
*/
|
|
export function sqliteType(field) {
|
|
const t = (field.type || "").toUpperCase();
|
|
if (t.includes("UUID") || t.includes("TEXT") || t.includes("VARCHAR") || t.includes("CHAR")) return "TEXT";
|
|
if (t.includes("INT") || t.includes("SERIAL") || t.includes("BIGINT")) return "INTEGER";
|
|
if (t.includes("DECIMAL") || t.includes("NUMERIC") || t.includes("FLOAT") || t.includes("DOUBLE") || t.includes("REAL")) return "REAL";
|
|
if (t.includes("BOOL")) return "INTEGER";
|
|
if (t.includes("DATE") || t.includes("TIME") || t.includes("TIMESTAMP")) return "TEXT";
|
|
if (t.includes("JSONB") || t.includes("JSON")) return "TEXT";
|
|
return "TEXT";
|
|
}
|
|
|
|
/**
|
|
* Get the non-auto fields for CreateInput (exclude id, created_at, updated_at).
|
|
*
|
|
* @param {object} entity — Contract entity
|
|
* @returns {object[]} Fields for CreateInput
|
|
*/
|
|
export function createInputFields(entity) {
|
|
return entity.fields.filter(f =>
|
|
!f.isPrimary && !f.isAuto && f.name !== "created_at" && f.name !== "updated_at"
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get the non-identity fields for UpdateInput (exclude id, created_at, updated_at).
|
|
*
|
|
* @param {object} entity — Contract entity
|
|
* @returns {object[]} Fields for UpdateInput
|
|
*/
|
|
export function updateInputFields(entity) {
|
|
return createInputFields(entity);
|
|
}
|
|
|
|
/**
|
|
* Check if an entity has the given field (by snake_case name).
|
|
*/
|
|
export function hasField(entity, snakeName) {
|
|
return entity.fields.some(f => f.name === snakeName);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
// 8. Export DOMAIN_ENTITIES for introspection
|
|
// ═══════════════════════════════════════════════════════
|
|
|
|
export { DOMAIN_ENTITIES };
|