@an5/orm 1.0.9 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -3
- package/dist/config.d.ts +9 -1
- package/dist/config.js +13 -18
- package/dist/generator/src/api.d.ts +35 -0
- package/dist/generator/src/api.js +58 -0
- package/dist/generator/src/config.d.ts +162 -0
- package/dist/generator/src/config.js +354 -0
- package/dist/generator/src/dialect.d.ts +88 -0
- package/dist/generator/src/dialect.js +318 -0
- package/dist/generator/src/dotnet-generator.d.ts +8 -0
- package/dist/generator/src/dotnet-generator.js +282 -101
- package/dist/generator/src/field-types.d.ts +96 -0
- package/dist/generator/src/field-types.js +433 -0
- package/dist/generator/src/golang-generator.d.ts +36 -0
- package/dist/generator/src/golang-generator.js +1230 -0
- package/dist/generator/src/index.js +69 -36
- package/dist/generator/src/metadata-generator.js +2 -1
- package/dist/generator/src/parser.d.ts +28 -2
- package/dist/generator/src/parser.js +92 -90
- package/dist/generator/src/push-apply.d.ts +37 -0
- package/dist/generator/src/push-apply.js +109 -0
- package/dist/generator/src/push-schema.d.ts +93 -0
- package/dist/generator/src/push-schema.js +189 -0
- package/dist/generator/src/python-generator.d.ts +19 -1
- package/dist/generator/src/python-generator.js +147 -64
- package/dist/generator/src/rust-generator.d.ts +38 -0
- package/dist/generator/src/rust-generator.js +971 -0
- package/dist/generator/src/suggest.d.ts +17 -0
- package/dist/generator/src/suggest.js +45 -0
- package/dist/generator/src/type-kinds.d.ts +27 -0
- package/dist/generator/src/type-kinds.js +72 -0
- package/dist/generator/src/types.d.ts +33 -0
- package/dist/generator/src/types.js +39 -0
- package/dist/migration-core.d.ts +16 -1
- package/dist/migration-core.js +60 -21
- package/dist/orm.d.ts +147 -0
- package/dist/orm.js +1654 -0
- package/dist/provider-support.d.ts +27 -0
- package/dist/provider-support.js +50 -0
- package/dist/sql-utils.js +8 -3
- package/generator/src/api.ts +45 -0
- package/generator/src/config.ts +496 -0
- package/generator/src/dialect.ts +415 -0
- package/generator/src/dotnet-generator.ts +277 -88
- package/generator/src/field-types.ts +473 -0
- package/generator/src/golang-generator.ts +94 -68
- package/generator/src/index.ts +71 -36
- package/generator/src/metadata-generator.ts +2 -2
- package/generator/src/parser.ts +122 -90
- package/generator/src/push-apply.ts +148 -0
- package/generator/src/push-schema.ts +250 -0
- package/generator/src/python-generator.ts +118 -62
- package/generator/src/rust-generator.ts +974 -0
- package/generator/src/suggest.ts +41 -0
- package/generator/src/type-kinds.ts +78 -0
- package/generator/src/types.ts +48 -0
- package/package.json +26 -6
|
@@ -0,0 +1,971 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.RustGenerator = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const types_1 = require("./types");
|
|
10
|
+
const type_kinds_1 = require("./type-kinds");
|
|
11
|
+
class RustGenerator {
|
|
12
|
+
constructor(outputDir) {
|
|
13
|
+
this.outputDir = outputDir;
|
|
14
|
+
if (!fs_1.default.existsSync(this.outputDir)) {
|
|
15
|
+
fs_1.default.mkdirSync(this.outputDir, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
const srcDir = path_1.default.join(this.outputDir, 'src');
|
|
18
|
+
if (!fs_1.default.existsSync(srcDir)) {
|
|
19
|
+
fs_1.default.mkdirSync(srcDir, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
generate(models) {
|
|
23
|
+
this.provider = models[0]?.provider;
|
|
24
|
+
this.generateCargoToml();
|
|
25
|
+
this.generateLibRs(models);
|
|
26
|
+
this.generateFiltersRs();
|
|
27
|
+
this.generateModelsRs(models);
|
|
28
|
+
this.generateMetadataRs(models);
|
|
29
|
+
this.generateConfigRs();
|
|
30
|
+
this.generateClientRs(models);
|
|
31
|
+
}
|
|
32
|
+
// ─── Cargo.toml ────────────────────────────────────────────────────────────
|
|
33
|
+
generateCargoToml() {
|
|
34
|
+
const content = `[package]
|
|
35
|
+
name = "an5-client"
|
|
36
|
+
version = "0.1.0"
|
|
37
|
+
edition = "2021"
|
|
38
|
+
description = "Generated AN5 ORM client for Rust — typed models, filters and an adapter-backed ORM client."
|
|
39
|
+
license = "MIT"
|
|
40
|
+
|
|
41
|
+
[dependencies]
|
|
42
|
+
serde = { version = "1", features = ["derive"] }
|
|
43
|
+
serde_json = "1"
|
|
44
|
+
chrono = { version = "0.4", features = ["serde"] }
|
|
45
|
+
an5-adapters = "0.1"
|
|
46
|
+
`;
|
|
47
|
+
fs_1.default.writeFileSync(path_1.default.join(this.outputDir, 'Cargo.toml'), content);
|
|
48
|
+
}
|
|
49
|
+
// ─── src/lib.rs ────────────────────────────────────────────────────────────
|
|
50
|
+
generateLibRs(models) {
|
|
51
|
+
const modList = ['filters', 'models', 'metadata', 'config', 'client'];
|
|
52
|
+
let content = `//! AN5 ORM generated Rust client.\n//! Code generated by an5ORM. DO NOT EDIT.\n\n`;
|
|
53
|
+
for (const m of modList) {
|
|
54
|
+
content += `pub mod ${m};\n`;
|
|
55
|
+
}
|
|
56
|
+
content += `\npub use filters::*;\n`;
|
|
57
|
+
content += `pub use models::*;\n`;
|
|
58
|
+
content += `pub use metadata::*;\n`;
|
|
59
|
+
content += `pub use config::*;\n`;
|
|
60
|
+
content += `pub use client::*;\n`;
|
|
61
|
+
fs_1.default.writeFileSync(path_1.default.join(this.outputDir, 'src', 'lib.rs'), content);
|
|
62
|
+
}
|
|
63
|
+
// ─── src/filters.rs ────────────────────────────────────────────────────────
|
|
64
|
+
generateFiltersRs() {
|
|
65
|
+
const content = `//! Shared filter types for AN5 Rust client.
|
|
66
|
+
//! Code generated by an5ORM. DO NOT EDIT.
|
|
67
|
+
|
|
68
|
+
use chrono::{DateTime, Utc};
|
|
69
|
+
use serde::{Deserialize, Serialize};
|
|
70
|
+
|
|
71
|
+
/// Sort direction.
|
|
72
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
73
|
+
#[serde(rename_all = "UPPERCASE")]
|
|
74
|
+
pub enum SortOrder {
|
|
75
|
+
Asc,
|
|
76
|
+
Desc,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
impl SortOrder {
|
|
80
|
+
pub fn as_sql(&self) -> &'static str {
|
|
81
|
+
match self {
|
|
82
|
+
SortOrder::Asc => "ASC",
|
|
83
|
+
SortOrder::Desc => "DESC",
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// Database dialect for identifier quoting / placeholders.
|
|
89
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
90
|
+
pub enum Dialect {
|
|
91
|
+
Mssql,
|
|
92
|
+
Postgres,
|
|
93
|
+
Sqlite,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
impl Dialect {
|
|
97
|
+
pub fn detect(conn_str: &str) -> Self {
|
|
98
|
+
let l = conn_str.trim().to_lowercase();
|
|
99
|
+
if l.starts_with("postgres://")
|
|
100
|
+
|| l.starts_with("postgresql://")
|
|
101
|
+
|| l.contains("port=5432")
|
|
102
|
+
{
|
|
103
|
+
return Dialect::Postgres;
|
|
104
|
+
}
|
|
105
|
+
if l.starts_with("sqlite://")
|
|
106
|
+
|| l.starts_with("sqlite:")
|
|
107
|
+
|| l.starts_with("file:")
|
|
108
|
+
|| l.ends_with(".db")
|
|
109
|
+
|| l.ends_with(".sqlite")
|
|
110
|
+
|| l.ends_with(".sqlite3")
|
|
111
|
+
|| l == ":memory:"
|
|
112
|
+
{
|
|
113
|
+
return Dialect::Sqlite;
|
|
114
|
+
}
|
|
115
|
+
Dialect::Mssql
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
pub fn quote_ident(&self, name: &str) -> String {
|
|
119
|
+
match self {
|
|
120
|
+
Dialect::Postgres | Dialect::Sqlite => {
|
|
121
|
+
format!("\\"{}\\"", name.replace('"', "\\"\\""))
|
|
122
|
+
}
|
|
123
|
+
Dialect::Mssql => format!("[{}]", name.replace(']', "]]")),
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
pub fn placeholder(&self, n: usize) -> String {
|
|
128
|
+
match self {
|
|
129
|
+
Dialect::Postgres => format!("\${}", n),
|
|
130
|
+
_ => "?".to_string(),
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/// A typed bind parameter produced by the query builders.
|
|
136
|
+
///
|
|
137
|
+
/// Values keep their Rust type so drivers can bind them natively (a Bool
|
|
138
|
+
/// must not become the string "true" when the column is an integer).
|
|
139
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
140
|
+
pub enum BindValue {
|
|
141
|
+
Text(String),
|
|
142
|
+
Int(i64),
|
|
143
|
+
Float(f64),
|
|
144
|
+
Bool(bool),
|
|
145
|
+
/// RFC 3339 timestamp.
|
|
146
|
+
DateTime(String),
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
impl BindValue {
|
|
150
|
+
/// Textual rendering, useful for logging or drivers that take strings.
|
|
151
|
+
pub fn as_str(&self) -> String {
|
|
152
|
+
match self {
|
|
153
|
+
BindValue::Text(v) => v.clone(),
|
|
154
|
+
BindValue::Int(v) => v.to_string(),
|
|
155
|
+
BindValue::Float(v) => v.to_string(),
|
|
156
|
+
BindValue::Bool(v) => v.to_string(),
|
|
157
|
+
BindValue::DateTime(v) => v.clone(),
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
impl std::fmt::Display for BindValue {
|
|
163
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
164
|
+
write!(f, "{}", self.as_str())
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
impl From<&str> for BindValue {
|
|
169
|
+
fn from(v: &str) -> Self {
|
|
170
|
+
BindValue::Text(v.to_string())
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
impl From<String> for BindValue {
|
|
175
|
+
fn from(v: String) -> Self {
|
|
176
|
+
BindValue::Text(v)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
impl From<i64> for BindValue {
|
|
181
|
+
fn from(v: i64) -> Self {
|
|
182
|
+
BindValue::Int(v)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
impl From<f64> for BindValue {
|
|
187
|
+
fn from(v: f64) -> Self {
|
|
188
|
+
BindValue::Float(v)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
impl From<bool> for BindValue {
|
|
193
|
+
fn from(v: bool) -> Self {
|
|
194
|
+
BindValue::Bool(v)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/// Type-safe filter for string fields.
|
|
199
|
+
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
|
200
|
+
#[serde(rename_all = "camelCase")]
|
|
201
|
+
pub struct StringFilter {
|
|
202
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
203
|
+
pub equals: Option<String>,
|
|
204
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
205
|
+
pub not: Option<String>,
|
|
206
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
207
|
+
pub contains: Option<String>,
|
|
208
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
209
|
+
pub starts_with: Option<String>,
|
|
210
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
211
|
+
pub ends_with: Option<String>,
|
|
212
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
213
|
+
pub gt: Option<String>,
|
|
214
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
215
|
+
pub gte: Option<String>,
|
|
216
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
217
|
+
pub lt: Option<String>,
|
|
218
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
219
|
+
pub lte: Option<String>,
|
|
220
|
+
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
221
|
+
pub in_list: Vec<String>,
|
|
222
|
+
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
223
|
+
pub not_in: Vec<String>,
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/// Type-safe filter for integer fields.
|
|
227
|
+
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
|
228
|
+
#[serde(rename_all = "camelCase")]
|
|
229
|
+
pub struct IntFilter {
|
|
230
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
231
|
+
pub equals: Option<i64>,
|
|
232
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
233
|
+
pub not: Option<i64>,
|
|
234
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
235
|
+
pub gt: Option<i64>,
|
|
236
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
237
|
+
pub gte: Option<i64>,
|
|
238
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
239
|
+
pub lt: Option<i64>,
|
|
240
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
241
|
+
pub lte: Option<i64>,
|
|
242
|
+
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
243
|
+
pub in_list: Vec<i64>,
|
|
244
|
+
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
245
|
+
pub not_in: Vec<i64>,
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/// Type-safe filter for float / decimal fields.
|
|
249
|
+
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
|
250
|
+
#[serde(rename_all = "camelCase")]
|
|
251
|
+
pub struct NumberFilter {
|
|
252
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
253
|
+
pub equals: Option<f64>,
|
|
254
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
255
|
+
pub not: Option<f64>,
|
|
256
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
257
|
+
pub gt: Option<f64>,
|
|
258
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
259
|
+
pub gte: Option<f64>,
|
|
260
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
261
|
+
pub lt: Option<f64>,
|
|
262
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
263
|
+
pub lte: Option<f64>,
|
|
264
|
+
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
265
|
+
pub in_list: Vec<f64>,
|
|
266
|
+
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
267
|
+
pub not_in: Vec<f64>,
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/// Deserializes a boolean that some engines store as 0/1 or "0"/"1"
|
|
271
|
+
/// (SQLite, MySQL TINYINT, MSSQL BIT via JSON).
|
|
272
|
+
pub fn deserialize_bool_flexible<'de, D>(deserializer: D) -> Result<bool, D::Error>
|
|
273
|
+
where
|
|
274
|
+
D: serde::Deserializer<'de>,
|
|
275
|
+
{
|
|
276
|
+
match serde_json::Value::deserialize(deserializer)? {
|
|
277
|
+
serde_json::Value::Bool(b) => Ok(b),
|
|
278
|
+
serde_json::Value::Number(n) => Ok(n.as_i64().unwrap_or(0) != 0),
|
|
279
|
+
serde_json::Value::String(s) => match s.trim().to_ascii_lowercase().as_str() {
|
|
280
|
+
"1" | "true" | "t" | "yes" | "y" => Ok(true),
|
|
281
|
+
"0" | "false" | "f" | "no" | "n" | "" => Ok(false),
|
|
282
|
+
_ => Err(serde::de::Error::custom(format!("cannot parse {s:?} as a boolean"))),
|
|
283
|
+
},
|
|
284
|
+
_ => Err(serde::de::Error::custom("expected a boolean value")),
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/// Optional variant of deserialize_bool_flexible.
|
|
289
|
+
pub fn deserialize_option_bool_flexible<'de, D>(deserializer: D) -> Result<Option<bool>, D::Error>
|
|
290
|
+
where
|
|
291
|
+
D: serde::Deserializer<'de>,
|
|
292
|
+
{
|
|
293
|
+
match serde_json::Value::deserialize(deserializer)? {
|
|
294
|
+
serde_json::Value::Null => Ok(None),
|
|
295
|
+
other => {
|
|
296
|
+
let as_bool = match other {
|
|
297
|
+
serde_json::Value::Bool(b) => b,
|
|
298
|
+
serde_json::Value::Number(n) => n.as_i64().unwrap_or(0) != 0,
|
|
299
|
+
serde_json::Value::String(s) => match s.trim().to_ascii_lowercase().as_str() {
|
|
300
|
+
"1" | "true" | "t" | "yes" | "y" => true,
|
|
301
|
+
"0" | "false" | "f" | "no" | "n" | "" => false,
|
|
302
|
+
_ => {
|
|
303
|
+
return Err(serde::de::Error::custom(format!(
|
|
304
|
+
"cannot parse {s:?} as a boolean"
|
|
305
|
+
)))
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
_ => return Err(serde::de::Error::custom("expected a boolean value")),
|
|
309
|
+
};
|
|
310
|
+
Ok(Some(as_bool))
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/// Type-safe filter for boolean fields.
|
|
316
|
+
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
|
317
|
+
#[serde(rename_all = "camelCase")]
|
|
318
|
+
pub struct BoolFilter {
|
|
319
|
+
#[serde(
|
|
320
|
+
default,
|
|
321
|
+
deserialize_with = "deserialize_option_bool_flexible",
|
|
322
|
+
skip_serializing_if = "Option::is_none"
|
|
323
|
+
)]
|
|
324
|
+
pub equals: Option<bool>,
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/// Type-safe filter for datetime fields.
|
|
328
|
+
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
|
329
|
+
#[serde(rename_all = "camelCase")]
|
|
330
|
+
pub struct DateTimeFilter {
|
|
331
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
332
|
+
pub equals: Option<DateTime<Utc>>,
|
|
333
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
334
|
+
pub not: Option<DateTime<Utc>>,
|
|
335
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
336
|
+
pub gt: Option<DateTime<Utc>>,
|
|
337
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
338
|
+
pub gte: Option<DateTime<Utc>>,
|
|
339
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
340
|
+
pub lt: Option<DateTime<Utc>>,
|
|
341
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
342
|
+
pub lte: Option<DateTime<Utc>>,
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ─── SQL fragment builders (shared) ─────────────────────────────────────────
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
`;
|
|
353
|
+
fs_1.default.writeFileSync(path_1.default.join(this.outputDir, 'src', 'filters.rs'), content);
|
|
354
|
+
}
|
|
355
|
+
// ─── src/models.rs ─────────────────────────────────────────────────────────
|
|
356
|
+
generateModelsRs(models) {
|
|
357
|
+
let content = `//! Entity structs + typed Where / OrderBy / Args.\n//! Code generated by an5ORM. DO NOT EDIT.\n\nuse chrono::{DateTime, Utc};\nuse serde::{Deserialize, Serialize};\nuse crate::filters::*;\n\n`;
|
|
358
|
+
let out = this.buildModelsBody(models);
|
|
359
|
+
fs_1.default.writeFileSync(path_1.default.join(this.outputDir, 'src', 'models.rs'), content + out);
|
|
360
|
+
}
|
|
361
|
+
buildModelsBody(models) {
|
|
362
|
+
let s = [];
|
|
363
|
+
for (const model of models) {
|
|
364
|
+
const name = this.capitalize(model.name);
|
|
365
|
+
if (model.description) {
|
|
366
|
+
s.push(`/// ${model.description}`);
|
|
367
|
+
}
|
|
368
|
+
s.push(`#[derive(Debug, Clone, Serialize, Deserialize)]`);
|
|
369
|
+
s.push(`pub struct ${name} {`);
|
|
370
|
+
for (const f of model.fields) {
|
|
371
|
+
// A field with a schema default (`@default(now())`) may come back NULL
|
|
372
|
+
// when the caller never set it, so it is optional here just like in
|
|
373
|
+
// CreateInput. Matches the Python generator's required-field rule.
|
|
374
|
+
const isOptional = f.isOptional || f.hasDefault;
|
|
375
|
+
const rsType = this.mapRustType(f, isOptional);
|
|
376
|
+
const snake = this.toSnakeCase(f.name);
|
|
377
|
+
if (f.description) {
|
|
378
|
+
s.push(` /// ${f.description}`);
|
|
379
|
+
}
|
|
380
|
+
if (isOptional) {
|
|
381
|
+
if (this.isBoolField(f)) {
|
|
382
|
+
s.push(` #[serde(default, deserialize_with = "deserialize_option_bool_flexible", skip_serializing_if = "Option::is_none")]`);
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
s.push(` #[serde(default, skip_serializing_if = "Option::is_none")]`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
else if (this.isBoolField(f)) {
|
|
389
|
+
s.push(` #[serde(deserialize_with = "deserialize_bool_flexible")]`);
|
|
390
|
+
}
|
|
391
|
+
s.push(` pub ${snake}: ${rsType},`);
|
|
392
|
+
}
|
|
393
|
+
for (const rel of model.relations) {
|
|
394
|
+
const relSnake = this.toSnakeCase(rel.name);
|
|
395
|
+
const relType = rel.isArray
|
|
396
|
+
? `Vec<super::models::${this.capitalize(rel.type)}>`
|
|
397
|
+
: `Option<Box<super::models::${this.capitalize(rel.type)}>>`;
|
|
398
|
+
s.push(` #[serde(default, skip_serializing_if = "Option::is_none")]`);
|
|
399
|
+
// Optional relations are always Option-wrapped for ergonomics.
|
|
400
|
+
if (rel.isArray) {
|
|
401
|
+
s.push(` pub ${relSnake}: Option<${relType}>,`);
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
s.push(` pub ${relSnake}: ${relType},`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
s.push(`}\n`);
|
|
408
|
+
// WhereInput
|
|
409
|
+
s.push(`/// Type-safe WHERE filter for ${name} queries.`);
|
|
410
|
+
s.push(`#[derive(Debug, Clone, Default, Serialize, Deserialize)]`);
|
|
411
|
+
s.push(`pub struct ${name}WhereInput {`);
|
|
412
|
+
s.push(` #[serde(default, rename = "AND", skip_serializing_if = "Option::is_none")]`);
|
|
413
|
+
s.push(` pub and: Option<Vec<${name}WhereInput>>,`);
|
|
414
|
+
s.push(` #[serde(default, rename = "OR", skip_serializing_if = "Option::is_none")]`);
|
|
415
|
+
s.push(` pub or: Option<Vec<${name}WhereInput>>,`);
|
|
416
|
+
s.push(` #[serde(default, rename = "NOT", skip_serializing_if = "Option::is_none")]`);
|
|
417
|
+
s.push(` pub not: Option<Box<${name}WhereInput>>,`);
|
|
418
|
+
for (const f of model.fields) {
|
|
419
|
+
const ft = this.getRustFilterType(f);
|
|
420
|
+
const snake = this.toSnakeCase(f.name);
|
|
421
|
+
s.push(` #[serde(default, skip_serializing_if = "Option::is_none")]`);
|
|
422
|
+
s.push(` pub ${snake}: Option<${ft}>,`);
|
|
423
|
+
}
|
|
424
|
+
s.push(`}\n`);
|
|
425
|
+
// OrderByInput
|
|
426
|
+
s.push(`/// Type-safe ORDER BY for ${name} queries.`);
|
|
427
|
+
s.push(`#[derive(Debug, Clone, Default, Serialize, Deserialize)]`);
|
|
428
|
+
s.push(`pub struct ${name}OrderByInput {`);
|
|
429
|
+
for (const f of model.fields) {
|
|
430
|
+
const snake = this.toSnakeCase(f.name);
|
|
431
|
+
s.push(` #[serde(default, skip_serializing_if = "Option::is_none")]`);
|
|
432
|
+
s.push(` pub ${snake}: Option<SortOrder>,`);
|
|
433
|
+
}
|
|
434
|
+
s.push(`}\n`);
|
|
435
|
+
// CreateInput (skip @id auto fields from required set via hasDefault)
|
|
436
|
+
s.push(`/// Typed data for creating a new ${name} record.`);
|
|
437
|
+
s.push(`#[derive(Debug, Clone, Serialize, Deserialize)]`);
|
|
438
|
+
s.push(`pub struct ${name}CreateInput {`);
|
|
439
|
+
for (const f of model.fields) {
|
|
440
|
+
if (f.isId && f.hasDefault)
|
|
441
|
+
continue;
|
|
442
|
+
const snake = this.toSnakeCase(f.name);
|
|
443
|
+
const isOpt = f.isOptional || f.hasDefault;
|
|
444
|
+
const rsType = this.mapRustType(f, isOpt);
|
|
445
|
+
if (isOpt) {
|
|
446
|
+
s.push(` #[serde(default, skip_serializing_if = "Option::is_none")]`);
|
|
447
|
+
}
|
|
448
|
+
s.push(` pub ${snake}: ${rsType},`);
|
|
449
|
+
}
|
|
450
|
+
s.push(`}\n`);
|
|
451
|
+
// UpdateInput (all non-id optional)
|
|
452
|
+
s.push(`/// Typed data for updating an existing ${name} record.`);
|
|
453
|
+
s.push(`#[derive(Debug, Clone, Default, Serialize, Deserialize)]`);
|
|
454
|
+
s.push(`pub struct ${name}UpdateInput {`);
|
|
455
|
+
for (const f of model.fields) {
|
|
456
|
+
if (f.isId)
|
|
457
|
+
continue;
|
|
458
|
+
const snake = this.toSnakeCase(f.name);
|
|
459
|
+
const rsType = this.mapRustType(f, true);
|
|
460
|
+
s.push(` #[serde(default, skip_serializing_if = "Option::is_none")]`);
|
|
461
|
+
s.push(` pub ${snake}: ${rsType},`);
|
|
462
|
+
}
|
|
463
|
+
s.push(`}\n`);
|
|
464
|
+
// FindManyArgs / FindFirstArgs / FindUniqueArgs / CountArgs
|
|
465
|
+
s.push(`/// ORM-style args for ${name}.find_many().`);
|
|
466
|
+
s.push(`#[derive(Debug, Clone, Default, Serialize, Deserialize)]`);
|
|
467
|
+
s.push(`pub struct ${name}FindManyArgs {`);
|
|
468
|
+
s.push(` #[serde(default, skip_serializing_if = "Option::is_none")]`);
|
|
469
|
+
s.push(` pub where_: Option<${name}WhereInput>,`);
|
|
470
|
+
s.push(` #[serde(default, skip_serializing_if = "Option::is_none")]`);
|
|
471
|
+
s.push(` pub order_by: Option<${name}OrderByInput>,`);
|
|
472
|
+
s.push(` #[serde(default, skip_serializing_if = "Option::is_none")]`);
|
|
473
|
+
s.push(` pub take: Option<i64>,`);
|
|
474
|
+
s.push(` #[serde(default)]`);
|
|
475
|
+
s.push(` pub skip: i64,`);
|
|
476
|
+
s.push(` #[serde(default, skip_serializing_if = "Option::is_none")]`);
|
|
477
|
+
s.push(` pub select: Option<Vec<String>>,`);
|
|
478
|
+
s.push(`}\n`);
|
|
479
|
+
s.push(`/// ORM-style args for ${name}.find_first().`);
|
|
480
|
+
s.push(`#[derive(Debug, Clone, Default, Serialize, Deserialize)]`);
|
|
481
|
+
s.push(`pub struct ${name}FindFirstArgs {`);
|
|
482
|
+
s.push(` #[serde(default, skip_serializing_if = "Option::is_none")]`);
|
|
483
|
+
s.push(` pub where_: Option<${name}WhereInput>,`);
|
|
484
|
+
s.push(` #[serde(default, skip_serializing_if = "Option::is_none")]`);
|
|
485
|
+
s.push(` pub order_by: Option<${name}OrderByInput>,`);
|
|
486
|
+
s.push(`}\n`);
|
|
487
|
+
s.push(`/// ORM-style args for ${name}.find_unique() / count() / delete().`);
|
|
488
|
+
s.push(`#[derive(Debug, Clone, Default, Serialize, Deserialize)]`);
|
|
489
|
+
s.push(`pub struct ${name}FindUniqueArgs {`);
|
|
490
|
+
s.push(` #[serde(default, skip_serializing_if = "Option::is_none")]`);
|
|
491
|
+
s.push(` pub where_: Option<${name}WhereInput>,`);
|
|
492
|
+
s.push(`}\n`);
|
|
493
|
+
// UpdateArgs carries the typed input alongside the where clause.
|
|
494
|
+
s.push(`/// ORM-style args for ${name}.update().`);
|
|
495
|
+
s.push(`#[derive(Debug, Clone, Default, Serialize, Deserialize)]`);
|
|
496
|
+
s.push(`pub struct ${name}UpdateArgs {`);
|
|
497
|
+
s.push(` #[serde(default, skip_serializing_if = "Option::is_none")]`);
|
|
498
|
+
s.push(` pub where_: Option<${name}WhereInput>,`);
|
|
499
|
+
s.push(` #[serde(default)]`);
|
|
500
|
+
s.push(` pub data: ${name}UpdateInput,`);
|
|
501
|
+
s.push(`}\n`);
|
|
502
|
+
}
|
|
503
|
+
return s.join('\n') + '\n';
|
|
504
|
+
}
|
|
505
|
+
// ─── src/metadata.rs ───────────────────────────────────────────────────────
|
|
506
|
+
generateMetadataRs(models) {
|
|
507
|
+
let content = `//! Model → table mapping.\n//! Code generated by an5ORM. DO NOT EDIT.\n\n`;
|
|
508
|
+
content += `/// Resolve a model alias (User, user, users, …) to \`schema.table\`.\n`;
|
|
509
|
+
content += `pub fn model_to_table(model: &str) -> Option<&'static str> {\n match model {\n`;
|
|
510
|
+
const seen = new Set();
|
|
511
|
+
for (const model of models) {
|
|
512
|
+
const full = (0, types_1.bracketedTableName)(model);
|
|
513
|
+
for (const prop of this.getAllPropertyVariations(model.name)) {
|
|
514
|
+
if (seen.has(prop))
|
|
515
|
+
continue;
|
|
516
|
+
seen.add(prop);
|
|
517
|
+
content += ` "${prop}" => Some("${full}"),\n`;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
content += ` _ => None,\n }\n}\n\n`;
|
|
521
|
+
// Full alias → table map, handed to the adapter so `db.table("user")`
|
|
522
|
+
// resolves exactly like `db.user` does in the other languages.
|
|
523
|
+
content += `/// Every model alias mapped to its physical table, for adapter registration.\n`;
|
|
524
|
+
content += `pub fn model_to_table_map() -> Vec<(&'static str, &'static str)> {\n vec![\n`;
|
|
525
|
+
const seenMap = new Set();
|
|
526
|
+
for (const model of models) {
|
|
527
|
+
const full = (0, types_1.bracketedTableName)(model);
|
|
528
|
+
for (const prop of this.getAllPropertyVariations(model.name)) {
|
|
529
|
+
if (seenMap.has(prop))
|
|
530
|
+
continue;
|
|
531
|
+
seenMap.add(prop);
|
|
532
|
+
content += ` ("${prop}", "${full}"),\n`;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
content += ` ]\n}\n\n`;
|
|
536
|
+
content += `/// Primary-key column (snake_case) for a model alias.\n`;
|
|
537
|
+
content += `pub fn model_primary_key(model: &str) -> Option<&'static str> {\n match model {\n`;
|
|
538
|
+
const seenPk = new Set();
|
|
539
|
+
for (const model of models) {
|
|
540
|
+
const idField = model.fields.find((f) => f.isId) ?? model.fields[0];
|
|
541
|
+
const pkCol = idField ? this.toSnakeCase(idField.name) : 'id';
|
|
542
|
+
for (const prop of this.getAllPropertyVariations(model.name)) {
|
|
543
|
+
if (seenPk.has(prop))
|
|
544
|
+
continue;
|
|
545
|
+
seenPk.add(prop);
|
|
546
|
+
content += ` "${prop}" => Some("${pkCol}"),\n`;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
content += ` _ => None,\n }\n}\n`;
|
|
550
|
+
// Field metadata, handed to the adapter so it can apply schema defaults
|
|
551
|
+
// (for example generating a UUID for a string @id marked @default(uuid())).
|
|
552
|
+
content += `/// Field metadata per model alias, for adapter registration.\n`;
|
|
553
|
+
content += `pub fn model_fields_map() -> Vec<(&'static str, serde_json::Value)> {\n vec![\n`;
|
|
554
|
+
const seenFields = new Set();
|
|
555
|
+
for (const model of models) {
|
|
556
|
+
for (const prop of this.getAllPropertyVariations(model.name)) {
|
|
557
|
+
if (seenFields.has(prop))
|
|
558
|
+
continue;
|
|
559
|
+
seenFields.add(prop);
|
|
560
|
+
const fieldEntries = model.fields.map((f) => {
|
|
561
|
+
const parts = [
|
|
562
|
+
`"ts": ${JSON.stringify(f.type + (f.isOptional ? '?' : ''))}`,
|
|
563
|
+
`"sql": ${JSON.stringify(f.sqlType)}`,
|
|
564
|
+
`"isId": ${f.isId}`,
|
|
565
|
+
`"isOptional": ${f.isOptional}`,
|
|
566
|
+
`"hasDefault": ${f.hasDefault}`,
|
|
567
|
+
];
|
|
568
|
+
if (f.description)
|
|
569
|
+
parts.push(`"description": ${JSON.stringify(f.description)}`);
|
|
570
|
+
return `"${f.name}": { ${parts.join(', ')} }`;
|
|
571
|
+
});
|
|
572
|
+
content += ' ("' + prop + '", serde_json::json!({ ' + fieldEntries.join(', ') + ' })),\n';
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
content += " ]\n}\n\n";
|
|
576
|
+
fs_1.default.writeFileSync(path_1.default.join(this.outputDir, 'src', 'metadata.rs'), content);
|
|
577
|
+
}
|
|
578
|
+
// ─── src/config.rs ─────────────────────────────────────────────────────────
|
|
579
|
+
generateConfigRs() {
|
|
580
|
+
const content = `//! Connection-string helpers.\n//! Code generated by an5ORM. DO NOT EDIT.\n\n/// Read DATABASE_URL or fall back to a local MSSQL default.\npub fn get_default_connection_string() -> String {\n std::env::var("DATABASE_URL").unwrap_or_else(|_| {\n "Server=localhost;Database=master;Trusted_Connection=True;TrustServerCertificate=True;"\n .to_string()\n })\n}\n`;
|
|
581
|
+
fs_1.default.writeFileSync(path_1.default.join(this.outputDir, 'src', 'config.rs'), content);
|
|
582
|
+
}
|
|
583
|
+
// ─── src/client.rs ─────────────────────────────────────────────────────────
|
|
584
|
+
generateClientRs(models) {
|
|
585
|
+
let content = `//! Typed model handles, SQL builders and vector math for the AN5 Rust client.
|
|
586
|
+
//! Code generated by an5ORM. DO NOT EDIT.
|
|
587
|
+
|
|
588
|
+
use an5_adapters::{
|
|
589
|
+
AdapterMetadata, An5Adapter, CountArgs, DeleteManyArgs, FindManyArgs, RowMap, TableClient,
|
|
590
|
+
UpdateArgs,
|
|
591
|
+
};
|
|
592
|
+
use serde::de::DeserializeOwned;
|
|
593
|
+
|
|
594
|
+
use crate::filters::*;
|
|
595
|
+
use crate::metadata::{model_fields_map, model_primary_key, model_to_table, model_to_table_map};
|
|
596
|
+
use crate::models::*;
|
|
597
|
+
|
|
598
|
+
/// Error type shared with the adapter runtime.
|
|
599
|
+
pub type Result<T> = an5_adapters::Result<T>;
|
|
600
|
+
|
|
601
|
+
/// Serializes an optional ORM input into the JSON shape the adapter expects.
|
|
602
|
+
fn to_value<T: serde::Serialize>(input: &Option<T>) -> Result<Option<serde_json::Value>> {
|
|
603
|
+
match input {
|
|
604
|
+
Some(v) => Ok(Some(serde_json::to_value(v)?)),
|
|
605
|
+
None => Ok(None),
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/// Serializes an input into the row map used for writes.
|
|
610
|
+
fn to_map<T: serde::Serialize>(input: &T) -> Result<RowMap> {
|
|
611
|
+
Ok(serde_json::to_value(input)?
|
|
612
|
+
.as_object()
|
|
613
|
+
.cloned()
|
|
614
|
+
.unwrap_or_default())
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/// Converts an adapter row into a generated model.
|
|
618
|
+
fn deserialize_row<T: DeserializeOwned>(row: RowMap) -> Result<T> {
|
|
619
|
+
Ok(serde_json::from_value(serde_json::Value::Object(row))?)
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/// Converts adapter rows into generated models.
|
|
623
|
+
fn deserialize_rows<T: DeserializeOwned>(rows: Vec<RowMap>) -> Result<Vec<T>> {
|
|
624
|
+
rows.into_iter().map(deserialize_row).collect()
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/// Main ORM context. Wraps the an5-adapters runtime.
|
|
628
|
+
#[derive(Clone)]
|
|
629
|
+
pub struct An5Client {
|
|
630
|
+
adapter: An5Adapter,
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
impl An5Client {
|
|
634
|
+
/// Connect and register this client's model to table mapping.
|
|
635
|
+
pub async fn connect(connection_string: &str) -> Result<Self> {
|
|
636
|
+
let adapter = An5Adapter::connect(connection_string).await?;
|
|
637
|
+
Ok(Self::from_adapter(adapter))
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/// Wrap an existing adapter, registering this client's metadata.
|
|
641
|
+
pub fn from_adapter(adapter: An5Adapter) -> Self {
|
|
642
|
+
let mut map = std::collections::HashMap::new();
|
|
643
|
+
for (alias, table) in model_to_table_map() {
|
|
644
|
+
map.insert(alias.to_string(), table.to_string());
|
|
645
|
+
}
|
|
646
|
+
let mut fields = std::collections::HashMap::new();
|
|
647
|
+
for (alias, field_meta) in model_fields_map() {
|
|
648
|
+
fields.insert(alias.to_string(), field_meta);
|
|
649
|
+
}
|
|
650
|
+
adapter.set_metadata(AdapterMetadata {
|
|
651
|
+
model_to_table: map,
|
|
652
|
+
model_fields: fields,
|
|
653
|
+
..Default::default()
|
|
654
|
+
});
|
|
655
|
+
Self { adapter }
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/// The underlying adapter, for raw SQL, views and transactions.
|
|
659
|
+
pub fn adapter(&self) -> &An5Adapter {
|
|
660
|
+
&self.adapter
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/// Dynamic access by model name, the equivalent of \`db.table("User")\`.
|
|
664
|
+
pub fn table(&self, name: &str) -> TableClient {
|
|
665
|
+
self.adapter.table(name)
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/// Point a model at a different physical table (schema-less engines).
|
|
669
|
+
pub fn with_table(self, model: &str, table: &str) -> Self {
|
|
670
|
+
self.adapter.add_table_override(model, table);
|
|
671
|
+
self
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/// Resolve the table for a model.
|
|
675
|
+
pub fn table_name(&self, model: &str) -> Option<String> {
|
|
676
|
+
model_to_table(model).map(|t| t.to_string())
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
pub fn primary_key(&self, model: &str) -> Option<&'static str> {
|
|
680
|
+
model_primary_key(model)
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/// Cosine similarity in [0,1]-ish range; 1.0 == identical direction.
|
|
685
|
+
pub fn cosine_similarity(a: &[f64], b: &[f64]) -> f64 {
|
|
686
|
+
if a.len() != b.len() || a.is_empty() {
|
|
687
|
+
return 0.0;
|
|
688
|
+
}
|
|
689
|
+
let mut dot = 0.0;
|
|
690
|
+
let mut m1 = 0.0;
|
|
691
|
+
let mut m2 = 0.0;
|
|
692
|
+
for i in 0..a.len() {
|
|
693
|
+
dot += a[i] * b[i];
|
|
694
|
+
m1 += a[i] * a[i];
|
|
695
|
+
m2 += b[i] * b[i];
|
|
696
|
+
}
|
|
697
|
+
if m1 == 0.0 || m2 == 0.0 {
|
|
698
|
+
return 0.0;
|
|
699
|
+
}
|
|
700
|
+
dot / (m1.sqrt() * m2.sqrt())
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/// Euclidean distance between two vectors.
|
|
704
|
+
pub fn euclidean_distance(a: &[f64], b: &[f64]) -> f64 {
|
|
705
|
+
a.iter()
|
|
706
|
+
.zip(b.iter())
|
|
707
|
+
.map(|(x, y)| (x - y) * (x - y))
|
|
708
|
+
.sum::<f64>()
|
|
709
|
+
.sqrt()
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/// Dot product between two vectors.
|
|
713
|
+
pub fn dot_product(a: &[f64], b: &[f64]) -> f64 {
|
|
714
|
+
a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/// Distance honouring the AN5 metric names.
|
|
718
|
+
pub fn vector_distance(a: &[f64], b: &[f64], metric: &str) -> f64 {
|
|
719
|
+
match metric.to_lowercase().as_str() {
|
|
720
|
+
"euclidean" => euclidean_distance(a, b),
|
|
721
|
+
"dot" => -dot_product(a, b),
|
|
722
|
+
_ => 1.0 - cosine_similarity(a, b),
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/// Standalone SQL builder for callers that prefer to drive their own driver.
|
|
727
|
+
#[derive(Clone)]
|
|
728
|
+
pub struct SqlBuilder {
|
|
729
|
+
pub connection_string: String,
|
|
730
|
+
pub dialect: Dialect,
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
impl SqlBuilder {
|
|
734
|
+
pub fn new(connection_string: Option<String>) -> Self {
|
|
735
|
+
let cs = connection_string.unwrap_or_else(crate::config::get_default_connection_string);
|
|
736
|
+
let dialect = Dialect::detect(&cs);
|
|
737
|
+
Self {
|
|
738
|
+
connection_string: cs,
|
|
739
|
+
dialect,
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
pub fn table_name(&self, model: &str) -> Option<String> {
|
|
744
|
+
model_to_table(model).map(|t| t.to_string())
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
pub fn primary_key(&self, model: &str) -> Option<&'static str> {
|
|
748
|
+
model_primary_key(model)
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
pub fn quote_ident(&self, name: &str) -> String {
|
|
752
|
+
self.dialect.quote_ident(name)
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
pub fn placeholder(&self, n: usize) -> String {
|
|
756
|
+
self.dialect.placeholder(n)
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
`;
|
|
761
|
+
for (const model of models) {
|
|
762
|
+
content += this.buildModelRuntimeHandle(model);
|
|
763
|
+
}
|
|
764
|
+
fs_1.default.writeFileSync(path_1.default.join(this.outputDir, 'src', 'client.rs'), content);
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Typed, executing accessors for a model.
|
|
768
|
+
*
|
|
769
|
+
* Rust has no dynamic property access, so `db.user` cannot exist the way it
|
|
770
|
+
* does in TypeScript or Python. Following the Go generator (which emits a
|
|
771
|
+
* real `ctx.User` field), each model gets a generated method returning a
|
|
772
|
+
* typed handle whose queries run through the an5-adapters runtime.
|
|
773
|
+
*/
|
|
774
|
+
buildModelRuntimeHandle(model) {
|
|
775
|
+
const name = this.capitalize(model.name);
|
|
776
|
+
const snake = this.toSnakeCase(model.name);
|
|
777
|
+
const lines = [];
|
|
778
|
+
lines.push(`/// Typed queries for ${name}. Obtained via \`An5Client::${snake}()\`.`);
|
|
779
|
+
lines.push(`#[derive(Clone)]`);
|
|
780
|
+
lines.push(`pub struct ${name}Table {`);
|
|
781
|
+
lines.push(` db: An5Client,`);
|
|
782
|
+
lines.push(`}`);
|
|
783
|
+
lines.push(``);
|
|
784
|
+
lines.push(`impl An5Client {`);
|
|
785
|
+
lines.push(` /// Typed handle for ${name}.`);
|
|
786
|
+
lines.push(` pub fn ${snake}(&self) -> ${name}Table {`);
|
|
787
|
+
lines.push(` ${name}Table { db: self.clone() }`);
|
|
788
|
+
lines.push(` }`);
|
|
789
|
+
lines.push(`}`);
|
|
790
|
+
lines.push(``);
|
|
791
|
+
lines.push(`impl ${name}Table {`);
|
|
792
|
+
lines.push(` /// The underlying adapter, for raw SQL or transactions.`);
|
|
793
|
+
lines.push(` pub fn adapter(&self) -> &An5Adapter { self.db.adapter() }`);
|
|
794
|
+
lines.push(``);
|
|
795
|
+
lines.push(` pub async fn find_many(&self, args: &${name}FindManyArgs) -> Result<Vec<${name}>> {`);
|
|
796
|
+
lines.push(` let rows = self`);
|
|
797
|
+
lines.push(` .db`);
|
|
798
|
+
lines.push(` .table("${model.name}")`);
|
|
799
|
+
lines.push(` .find_many(&FindManyArgs {`);
|
|
800
|
+
lines.push(` r#where: to_value(&args.where_)?,`);
|
|
801
|
+
lines.push(` order_by: to_value(&args.order_by)?,`);
|
|
802
|
+
lines.push(` take: args.take.unwrap_or(0),`);
|
|
803
|
+
lines.push(` skip: args.skip,`);
|
|
804
|
+
lines.push(` select: args.select.as_ref().map(|s| serde_json::json!(s)),`);
|
|
805
|
+
lines.push(` ..Default::default()`);
|
|
806
|
+
lines.push(` })`);
|
|
807
|
+
lines.push(` .await?;`);
|
|
808
|
+
lines.push(` deserialize_rows(rows)`);
|
|
809
|
+
lines.push(` }`);
|
|
810
|
+
lines.push(``);
|
|
811
|
+
lines.push(` pub async fn find_first(&self, args: &${name}FindFirstArgs) -> Result<Option<${name}>> {`);
|
|
812
|
+
lines.push(` let row = self`);
|
|
813
|
+
lines.push(` .db`);
|
|
814
|
+
lines.push(` .table("${model.name}")`);
|
|
815
|
+
lines.push(` .find_first(&FindManyArgs {`);
|
|
816
|
+
lines.push(` r#where: to_value(&args.where_)?,`);
|
|
817
|
+
lines.push(` order_by: to_value(&args.order_by)?,`);
|
|
818
|
+
lines.push(` ..Default::default()`);
|
|
819
|
+
lines.push(` })`);
|
|
820
|
+
lines.push(` .await?;`);
|
|
821
|
+
lines.push(` match row {`);
|
|
822
|
+
lines.push(` Some(r) => Ok(Some(deserialize_row(r)?)),`);
|
|
823
|
+
lines.push(` None => Ok(None),`);
|
|
824
|
+
lines.push(` }`);
|
|
825
|
+
lines.push(` }`);
|
|
826
|
+
lines.push(``);
|
|
827
|
+
lines.push(` pub async fn find_unique(&self, args: &${name}FindUniqueArgs) -> Result<Option<${name}>> {`);
|
|
828
|
+
lines.push(` let row = self`);
|
|
829
|
+
lines.push(` .db`);
|
|
830
|
+
lines.push(` .table("${model.name}")`);
|
|
831
|
+
lines.push(` .find_unique(&FindManyArgs {`);
|
|
832
|
+
lines.push(` r#where: to_value(&args.where_)?,`);
|
|
833
|
+
lines.push(` ..Default::default()`);
|
|
834
|
+
lines.push(` })`);
|
|
835
|
+
lines.push(` .await?;`);
|
|
836
|
+
lines.push(` match row {`);
|
|
837
|
+
lines.push(` Some(r) => Ok(Some(deserialize_row(r)?)),`);
|
|
838
|
+
lines.push(` None => Ok(None),`);
|
|
839
|
+
lines.push(` }`);
|
|
840
|
+
lines.push(` }`);
|
|
841
|
+
lines.push(``);
|
|
842
|
+
lines.push(` pub async fn count(&self, args: &${name}FindUniqueArgs) -> Result<i64> {`);
|
|
843
|
+
lines.push(` self.db`);
|
|
844
|
+
lines.push(` .table("${model.name}")`);
|
|
845
|
+
lines.push(` .count(&CountArgs { r#where: to_value(&args.where_)? })`);
|
|
846
|
+
lines.push(` .await`);
|
|
847
|
+
lines.push(` }`);
|
|
848
|
+
lines.push(``);
|
|
849
|
+
lines.push(` pub async fn create(&self, data: &${name}CreateInput) -> Result<${name}> {`);
|
|
850
|
+
lines.push(` let row = self`);
|
|
851
|
+
lines.push(` .db`);
|
|
852
|
+
lines.push(` .table("${model.name}")`);
|
|
853
|
+
lines.push(` .create(&to_map(data)?)`);
|
|
854
|
+
lines.push(` .await?;`);
|
|
855
|
+
lines.push(` deserialize_row(row)`);
|
|
856
|
+
lines.push(` }`);
|
|
857
|
+
lines.push(``);
|
|
858
|
+
lines.push(` pub async fn update(&self, args: &${name}UpdateArgs) -> Result<u64> {`);
|
|
859
|
+
lines.push(` self.db`);
|
|
860
|
+
lines.push(` .table("${model.name}")`);
|
|
861
|
+
lines.push(` .update(&UpdateArgs {`);
|
|
862
|
+
lines.push(` r#where: to_value(&args.where_)?,`);
|
|
863
|
+
lines.push(` data: serde_json::to_value(&args.data)?,`);
|
|
864
|
+
lines.push(` })`);
|
|
865
|
+
lines.push(` .await`);
|
|
866
|
+
lines.push(` }`);
|
|
867
|
+
lines.push(``);
|
|
868
|
+
lines.push(` pub async fn delete(&self, args: &${name}FindUniqueArgs) -> Result<u64> {`);
|
|
869
|
+
lines.push(` self.db`);
|
|
870
|
+
lines.push(` .table("${model.name}")`);
|
|
871
|
+
lines.push(` .delete_many(&DeleteManyArgs { r#where: to_value(&args.where_)? })`);
|
|
872
|
+
lines.push(` .await`);
|
|
873
|
+
lines.push(` }`);
|
|
874
|
+
lines.push(`}`);
|
|
875
|
+
lines.push(``);
|
|
876
|
+
return lines.join('\n') + '\n';
|
|
877
|
+
}
|
|
878
|
+
isBoolField(field) {
|
|
879
|
+
return ['bool', 'boolean', 'bit'].includes(field.type.toLowerCase());
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* The Rust type for a field.
|
|
883
|
+
*
|
|
884
|
+
* From the declared type, so `DECIMAL` is an `f64` and `BIGINT` an `i64`: both
|
|
885
|
+
* arrived as `number` and were written as `i64`.
|
|
886
|
+
*/
|
|
887
|
+
mapRustType(field, isOptional) {
|
|
888
|
+
let t;
|
|
889
|
+
switch ((0, type_kinds_1.fieldKind)(field, this.provider)) {
|
|
890
|
+
case 'int':
|
|
891
|
+
t = 'i32';
|
|
892
|
+
break;
|
|
893
|
+
case 'bigint':
|
|
894
|
+
t = 'i64';
|
|
895
|
+
break;
|
|
896
|
+
case 'float':
|
|
897
|
+
t = 'f64';
|
|
898
|
+
break;
|
|
899
|
+
case 'bool':
|
|
900
|
+
t = 'bool';
|
|
901
|
+
break;
|
|
902
|
+
case 'date':
|
|
903
|
+
t = 'DateTime<Utc>';
|
|
904
|
+
break;
|
|
905
|
+
case 'bytes':
|
|
906
|
+
t = 'Vec<u8>';
|
|
907
|
+
break;
|
|
908
|
+
case 'json':
|
|
909
|
+
t = 'serde_json::Value';
|
|
910
|
+
break;
|
|
911
|
+
case 'vector':
|
|
912
|
+
t = 'Vec<f32>';
|
|
913
|
+
break;
|
|
914
|
+
default:
|
|
915
|
+
t = 'String';
|
|
916
|
+
break;
|
|
917
|
+
}
|
|
918
|
+
if (isOptional)
|
|
919
|
+
return `Option<${t}>`;
|
|
920
|
+
return t;
|
|
921
|
+
}
|
|
922
|
+
getRustFilterType(field) {
|
|
923
|
+
// From the declared type: both arrived as `number` and picked the integer filter.
|
|
924
|
+
switch ((0, type_kinds_1.fieldKind)(field, this.provider)) {
|
|
925
|
+
case 'date': return 'DateTimeFilter';
|
|
926
|
+
case 'bool': return 'BoolFilter';
|
|
927
|
+
case 'int':
|
|
928
|
+
case 'bigint': return 'IntFilter';
|
|
929
|
+
case 'float': return 'NumberFilter';
|
|
930
|
+
default: return 'StringFilter';
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
getAllPropertyVariations(modelName) {
|
|
934
|
+
const variations = new Set();
|
|
935
|
+
variations.add(modelName);
|
|
936
|
+
variations.add(this.toCamelCase(modelName));
|
|
937
|
+
variations.add(modelName + 's');
|
|
938
|
+
variations.add(this.toCamelCase(modelName) + 's');
|
|
939
|
+
variations.add(this.toSnakeCase(modelName));
|
|
940
|
+
variations.add(this.toSnakeCase(modelName) + 's');
|
|
941
|
+
const acronyms = ['LLM', 'AI', 'MCP', 'IT', 'QC', 'HR', 'MR', 'WH', 'SSIS', 'API', 'URL', 'ID', 'JSON'];
|
|
942
|
+
for (const acronym of acronyms) {
|
|
943
|
+
if (modelName.startsWith(acronym)) {
|
|
944
|
+
variations.add(acronym.toLowerCase() + modelName.slice(acronym.length));
|
|
945
|
+
variations.add(acronym[0].toLowerCase() + acronym.slice(1) + modelName.slice(acronym.length));
|
|
946
|
+
variations.add(acronym.toLowerCase() + modelName.slice(acronym.length) + 's');
|
|
947
|
+
variations.add(this.toSnakeCase(acronym.toLowerCase() + modelName.slice(acronym.length)));
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
return Array.from(variations);
|
|
951
|
+
}
|
|
952
|
+
capitalize(str) {
|
|
953
|
+
if (!str)
|
|
954
|
+
return '';
|
|
955
|
+
return str[0].toUpperCase() + str.slice(1);
|
|
956
|
+
}
|
|
957
|
+
toCamelCase(str) {
|
|
958
|
+
if (!str)
|
|
959
|
+
return '';
|
|
960
|
+
return str[0].toLowerCase() + str.slice(1);
|
|
961
|
+
}
|
|
962
|
+
toSnakeCase(str) {
|
|
963
|
+
if (!str)
|
|
964
|
+
return '';
|
|
965
|
+
return str
|
|
966
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
|
|
967
|
+
.replace(/([a-z\d])([A-Z])/g, '$1_$2')
|
|
968
|
+
.toLowerCase();
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
exports.RustGenerator = RustGenerator;
|