Skip to content

Commit 6187d41

Browse files
authored
Automatic deployment testing for Javascript SDK (#788)
1 parent 2a02e46 commit 6187d41

File tree

13 files changed

+395
-22
lines changed

13 files changed

+395
-22
lines changed

.github/workflows/javascript-sdk.yml

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
name: deploy javascript sdk
2+
on:
3+
workflow_dispatch:
4+
jobs:
5+
build-javascript-sdk-linux:
6+
strategy:
7+
matrix:
8+
os: ["ubuntu-22.04", "buildjet-4vcpu-ubuntu-2204-arm", "macos-latest", "windows-latest"]
9+
include:
10+
- neon-out-name: "x86_64-unknown-linux-gnu-index.node"
11+
os: "ubuntu-22.04"
12+
- neon-out-name: "aarch64-unknown-linux-gnu-index.node"
13+
os: "buildjet-4vcpu-ubuntu-2204-arm"
14+
- neon-out-name: "x86_64-apple-darwin-index.node"
15+
os: "macos-latest"
16+
- neon-out-name: "x86_64-pc-windows-gnu.node"
17+
os: "windows-latest"
18+
runs-on: ${{ matrix.os }}
19+
defaults:
20+
run:
21+
working-directory: pgml-sdks/rust/pgml/javascript
22+
steps:
23+
- uses: actions/checkout@v3
24+
- uses: actions-rs/toolchain@v1
25+
with:
26+
toolchain: stable
27+
- name: Validate cargo is working
28+
uses: postgresml/gh-actions-cargo@master
29+
with:
30+
command: version
31+
- name: Do build
32+
env:
33+
NEON_OUT_NAME: ${{ matrix.neon-out-name }}
34+
run: |
35+
npm i
36+
npm run build-named
37+
- name: Upload built .node file
38+
uses: actions/upload-artifact@v3
39+
with:
40+
name: node-artifacts
41+
path: ${{ matrix.neon-out-name }}
42+
retention-days: 1
43+
publish-javascript-sdk:
44+
runs-on: "ubuntu-22.04"
45+
defaults:
46+
run:
47+
working-directory: pgml-sdks/rust/pgml/javascript
48+
steps:
49+
- name: Create artifact directory
50+
run: mkdir dist
51+
- uses: actions/download-artifact@v3
52+
with:
53+
name: node-artifacts
54+
path: dist
55+
- name: Display structure of download-artifacts
56+
run: ls -R dist

pgml-sdks/rust/pgml-macros/src/javascript.rs

Lines changed: 120 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
use quote::{format_ident, quote, ToTokens};
22
use syn::{visit::Visit, DeriveInput, ItemImpl, Type};
3+
use std::fs::OpenOptions;
4+
use std::io::{Read, Write};
35

46
use crate::common::{AttributeArgs, GetImplMethod};
5-
use crate::types::{OutputType, SupportedType};
7+
use crate::types::{OutputType, SupportedType, GetSupportedType};
68

79
pub fn generate_custom_into_js_result(parsed: DeriveInput) -> proc_macro::TokenStream {
810
let name = parsed.ident;
@@ -14,19 +16,43 @@ pub fn generate_custom_into_js_result(parsed: DeriveInput) -> proc_macro::TokenS
1416
_ => panic!("custom_into_js proc_macro should only be used on structs"),
1517
};
1618

17-
let sets: Vec<proc_macro2::TokenStream> = fields_named
19+
let mut sets = Vec::new();
20+
let mut interface = format!("\ninterface {} {{\n", name);
21+
22+
fields_named
1823
.named
1924
.into_pairs()
20-
.map(|p| {
25+
.for_each(|p| {
2126
let v = p.into_value();
2227
let name = v.ident.to_token_stream().to_string();
2328
let name_ident = v.ident;
24-
quote! {
29+
sets.push(quote! {
2530
let js_item = self.#name_ident.into_js_result(cx)?;
2631
js_object.set(cx, #name, js_item)?;
27-
}
28-
})
29-
.collect();
32+
});
33+
let ty = GetSupportedType::get_type(&v.ty);
34+
let decleration = match &ty {
35+
SupportedType::Option(o) => format!("{}?", get_typescript_type(o)),
36+
_ => get_typescript_type(&ty)
37+
};
38+
interface.push_str(&format!("\t{}: {},\n", name, decleration));
39+
});
40+
41+
interface.push('}');
42+
let mut file = OpenOptions::new()
43+
.create(true)
44+
.write(true)
45+
.append(true)
46+
.read(true)
47+
.open("javascript/index.d.ts")
48+
.unwrap();
49+
let mut contents = String::new();
50+
file.read_to_string(&mut contents)
51+
.expect("Unable to read typescript decleration file");
52+
if !contents.contains(&interface) {
53+
file.write_all(interface.as_bytes())
54+
.expect("Unable to write typescript decleration file");
55+
}
3056

3157
let out = quote! {
3258
impl IntoJsResult for #name {
@@ -77,6 +103,9 @@ pub fn generate_javascript_methods(
77103
};
78104
let name_ident = format_ident!("{}Javascript", wrapped_type_ident);
79105

106+
let javascript_class_name = wrapped_type_ident.to_string();
107+
let mut typescript_declarations = format!("\ndeclare class {} {{\n", javascript_class_name);
108+
80109
// Iterate over the items - see: https://docs.rs/syn/latest/syn/enum.ImplItem.html
81110
for item in parsed.items {
82111
// We only create methods for functions listed in the attribute args
@@ -106,6 +135,34 @@ pub fn generate_javascript_methods(
106135
OutputType::Default => (None, None),
107136
};
108137

138+
let p1 = method_ident.to_string();
139+
let p2 = method
140+
.method_arguments
141+
.iter()
142+
.filter(|a| !matches!(a.1, SupportedType::S))
143+
.map(|a| {
144+
match &a.1 {
145+
SupportedType::Option(o) => format!("{}?: {}", a.0, get_typescript_type(o)),
146+
_ => format!("{}: {}", a.0, get_typescript_type(&a.1))
147+
}
148+
})
149+
.collect::<Vec<String>>()
150+
.join(", ");
151+
let p3 = match &method.output_type {
152+
OutputType::Result(v) | OutputType::Other(v) => {
153+
match v {
154+
SupportedType::S => wrapped_type_ident.to_string(),
155+
_ => get_typescript_type(v),
156+
}
157+
},
158+
OutputType::Default => "void".to_string(),
159+
};
160+
if method.is_async {
161+
typescript_declarations.push_str(&format!("\n\t{}({}): Promise<{}>;\n", p1, p2, p3));
162+
} else {
163+
typescript_declarations.push_str(&format!("\n\t{}({}): {};\n", p1, p2, p3));
164+
}
165+
109166
let method_name_string = method_ident.to_string();
110167
object_sets.push(quote! {
111168
let f: Handle<JsFunction> = JsFunction::new(cx, #name_ident::#method_ident)?;
@@ -193,6 +250,23 @@ pub fn generate_javascript_methods(
193250
methods.push(mq);
194251
}
195252

253+
typescript_declarations.push('}');
254+
255+
let mut file = OpenOptions::new()
256+
.create(true)
257+
.write(true)
258+
.append(true)
259+
.read(true)
260+
.open("javascript/index.d.ts")
261+
.unwrap();
262+
let mut contents = String::new();
263+
file.read_to_string(&mut contents)
264+
.expect("Unable to read typescript declaration file for python");
265+
if !contents.contains(&format!("declare class {}", javascript_class_name)) {
266+
file.write_all(typescript_declarations.as_bytes())
267+
.expect("Unable to write typescript declaration file for python");
268+
}
269+
196270
proc_macro::TokenStream::from(quote! {
197271
impl #name_ident {
198272
#(#methods)*
@@ -230,7 +304,7 @@ fn get_method_wrapper_arguments_javascript(
230304
argument_ident.clone(),
231305
argument_type,
232306
);
233-
let argument_type_js = get_javascript_type(argument_type);
307+
let argument_type_js = get_neon_type(argument_type);
234308
let method_argument = match argument_type {
235309
SupportedType::Option(_o) => quote! {
236310
let #argument_ident = cx.argument_opt(#i as i32);
@@ -267,9 +341,9 @@ fn convert_method_wrapper_arguments(
267341
}
268342
}
269343

270-
fn get_javascript_type(ty: &SupportedType) -> syn::Type {
344+
fn get_neon_type(ty: &SupportedType) -> syn::Type {
271345
match ty {
272-
SupportedType::Reference(r) => get_javascript_type(r),
346+
SupportedType::Reference(r) => get_neon_type(r),
273347
SupportedType::str | SupportedType::String => syn::parse_str("JsString").unwrap(),
274348
SupportedType::Vec(_v) => syn::parse_str("JsArray").unwrap(),
275349
SupportedType::S => syn::parse_str("JsObject").unwrap(),
@@ -285,7 +359,7 @@ fn get_javascript_type(ty: &SupportedType) -> syn::Type {
285359
}
286360
}
287361

288-
pub fn convert_output_type_convert_from_javascript(
362+
fn convert_output_type_convert_from_javascript(
289363
ty: &SupportedType,
290364
method: &GetImplMethod,
291365
) -> (
@@ -302,7 +376,7 @@ pub fn convert_output_type_convert_from_javascript(
302376
Some(format_ident!("{}Javascript", t.to_string()).into_token_stream()),
303377
),
304378
t => {
305-
let ty = get_javascript_type(t);
379+
let ty = get_neon_type(t);
306380
(Some(quote! {JsResult<'a, #ty>}), None)
307381
}
308382
};
@@ -313,3 +387,37 @@ pub fn convert_output_type_convert_from_javascript(
313387
(output_type, convert_from)
314388
}
315389
}
390+
391+
fn get_typescript_type(ty: &SupportedType) -> String {
392+
match ty {
393+
SupportedType::Reference(r) => get_typescript_type(r),
394+
SupportedType::str | SupportedType::String => "string".to_string(),
395+
SupportedType::Option(o) => get_typescript_type(o),
396+
SupportedType::Vec(v) => format!("{}[]", get_typescript_type(v)),
397+
SupportedType::HashMap((k, v)) => {
398+
format!("Map<{}, {}>", get_typescript_type(k), get_typescript_type(v))
399+
},
400+
SupportedType::JsonHashMap => "Map<string, string>".to_string(),
401+
SupportedType::DateTime => "Date".to_string(),
402+
SupportedType::Tuple(t) => {
403+
let mut types = Vec::new();
404+
for ty in t {
405+
types.push(get_typescript_type(ty));
406+
}
407+
// Rust's unit type is represented as an empty tuple
408+
if types.is_empty() {
409+
"void".to_string()
410+
} else {
411+
format!("[{}]", types.join(", "))
412+
}
413+
}
414+
SupportedType::i64 | SupportedType::f64 => "number".to_string(),
415+
// Our own types
416+
t @ SupportedType::Database
417+
| t @ SupportedType::Collection
418+
| t @ SupportedType::Splitter => t.to_string(),
419+
| t @ SupportedType::Model => t.to_string(),
420+
// Add more types as required
421+
_ => "any".to_string(),
422+
}
423+
}

pgml-sdks/rust/pgml/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ node_modules/
1313

1414
# Distribution / packaging
1515
.Python
16+
*.pyi
1617
build/
1718
develop-eggs/
1819
dist/

pgml-sdks/rust/pgml/build.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,20 @@
11
use std::fs::remove_file;
2+
use std::fs::OpenOptions;
3+
use std::io::Write;
24

35
fn main() {
46
// Remove python stub file that is auto generated each build
5-
remove_file("python/pgml/pgml.pyi").ok();
7+
remove_file("./python/pgml/pgml.pyi").ok();
8+
9+
// Remove typescript declaration file that is auto generated each build
10+
remove_file("./javascript/index.d.ts").ok();
11+
let mut file = OpenOptions::new()
12+
.create(true)
13+
.write(true)
14+
.append(true)
15+
.open("./javascript/index.d.ts")
16+
.unwrap();
17+
// Add our opening function declaration here
18+
file.write_all(b"\nexport function newDatabase(connection_string: string): Promise<Database>;\n")
19+
.unwrap();
620
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
const os = require("os")
2+
3+
const type = os.type()
4+
const arch = os.arch()
5+
6+
// if (type == "Darwin" && arch == "arm64") {
7+
// const pgml = require("./index.node")
8+
// module.exports = pgml
9+
// } else {
10+
// console.log("UNSUPPORTED TYPE OR ARCH:", type, arch)
11+
// }
12+
13+
14+
const pgml = require("./index.node")
15+
module.exports = pgml

pgml-sdks/rust/pgml/javascript/package-lock.json

Lines changed: 7 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pgml-sdks/rust/pgml/javascript/package.json

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,17 @@
22
"name": "pgml",
33
"version": "0.1.0",
44
"description": "",
5-
"main": "index.node",
6-
"type": "module",
5+
"main": "index.js",
76
"scripts": {
87
"build": "cargo-cp-artifact -nc index.node -- cargo build --message-format=json-render-diagnostics",
98
"build-debug": "npm run build --",
109
"build-release": "npm run build -- --release",
11-
"install": "npm run build-release",
12-
"test": "cargo test"
10+
"build-named": "cargo-cp-artifact -nc $NEON_OUT_NAME -- cargo build --message-format=json-render-diagnostics --release"
1311
},
14-
"author": "",
15-
"license": "ISC",
12+
"author": "PostgresML",
13+
"license": "MIT",
1614
"devDependencies": {
15+
"@types/node": "^20.3.1",
1716
"cargo-cp-artifact": "^0.1"
1817
}
1918
}

pgml-sdks/rust/pgml/javascript/tests/test.js renamed to pgml-sdks/rust/pgml/javascript/tests/javascript-tests/test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const pgml = require('../index.node');
1+
import pgml from '../../index.js'
22

33
const CONNECTION_STRING = process.env.DATABASE_URL;
44

pgml-sdks/rust/pgml/javascript/tests/package-lock.json

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)