@teamkeel/wasm 0.0.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/encodeWasm.js ADDED
@@ -0,0 +1,30 @@
1
+ const fs = require("fs");
2
+
3
+ const wasmCode = fs.readFileSync(__dirname + "/dist/keel.wasm");
4
+ const encoded = Buffer.from(wasmCode, "binary").toString("base64");
5
+
6
+ fs.writeFileSync(
7
+ __dirname + "/dist/wasm.js",
8
+ `
9
+ function asciiToBinary(str) {
10
+ if (typeof atob === "function") {
11
+ // this works in the browser
12
+ return atob(str);
13
+ } else {
14
+ // this works in node
15
+ return new Buffer(str, "base64").toString("binary");
16
+ }
17
+ }
18
+
19
+ function decode(encoded) {
20
+ var binaryString = asciiToBinary(encoded);
21
+ var bytes = new Uint8Array(binaryString.length);
22
+ for (var i = 0; i < binaryString.length; i++) {
23
+ bytes[i] = binaryString.charCodeAt(i);
24
+ }
25
+ return bytes.buffer;
26
+ }
27
+
28
+ module.exports.wasm = decode("${encoded}");
29
+ `
30
+ );
package/index.d.ts ADDED
@@ -0,0 +1,70 @@
1
+ export function format(schema: string): Promise<string>;
2
+
3
+ export function validate(req: ValidateRequest): Promise<ValidationResult>;
4
+
5
+ export function completions(
6
+ req: GetCompletionsRequest
7
+ ): Promise<CompletionResult>;
8
+
9
+ export function getDefinition(
10
+ req: GetDefinitionRequest
11
+ ): Promise<DefinitionResult>;
12
+
13
+ export interface DefinitionResult {
14
+ schema?: Position;
15
+ function?: { name: string };
16
+ }
17
+
18
+ export interface SchemaDefinition {
19
+ schema: SchemaDefinition;
20
+ }
21
+
22
+ export interface GetCompletionsRequest {
23
+ position: Position;
24
+ schemaFiles: SchemaFile[];
25
+ config?: string;
26
+ }
27
+
28
+ export interface GetDefinitionRequest {
29
+ position: Position;
30
+ schemaFiles: SchemaFile[];
31
+ }
32
+
33
+ export interface ValidateRequest {
34
+ schemaFiles: SchemaFile[];
35
+ config?: string;
36
+ }
37
+
38
+ export interface SchemaFile {
39
+ filename: string;
40
+ contents: string;
41
+ }
42
+
43
+ export interface Position {
44
+ filename: string;
45
+ line: number;
46
+ column: number;
47
+ }
48
+
49
+ export interface CompletionItem {
50
+ description: string;
51
+ label: string;
52
+ insertText: string;
53
+ kind: string;
54
+ }
55
+
56
+ export interface CompletionResult {
57
+ completions: CompletionItem[];
58
+ }
59
+
60
+ export interface ValidationError {
61
+ code: string;
62
+ pos: Position;
63
+ endPos: Position;
64
+ hint: string;
65
+ message: string;
66
+ }
67
+
68
+ export interface ValidationResult {
69
+ errors: ValidationError[];
70
+ }
package/index.js ADDED
@@ -0,0 +1,43 @@
1
+ require("./lib/wasm_exec_node");
2
+
3
+ const { wasm } = require("./dist/wasm.js");
4
+
5
+ async function keel() {
6
+ if (globalThis.keel) {
7
+ return globalThis.keel;
8
+ }
9
+
10
+ const go = new globalThis.Go();
11
+
12
+ const wasmModule = await WebAssembly.instantiate(wasm, go.importObject);
13
+ go.run(wasmModule.instance);
14
+
15
+ return globalThis.keel;
16
+ }
17
+
18
+ async function format() {
19
+ const api = await keel();
20
+ return api.format(...arguments);
21
+ }
22
+
23
+ async function validate() {
24
+ const api = await keel();
25
+ return api.validate(...arguments);
26
+ }
27
+
28
+ async function completions() {
29
+ const api = await keel();
30
+ return api.completions(...arguments);
31
+ }
32
+
33
+ async function getDefinition() {
34
+ const api = await keel();
35
+ return api.getDefinition(...arguments);
36
+ }
37
+
38
+ module.exports = {
39
+ format,
40
+ validate,
41
+ completions,
42
+ getDefinition,
43
+ };
package/index.test.ts ADDED
@@ -0,0 +1,219 @@
1
+ import { format, validate, completions, getDefinition } from "./index";
2
+ import { test, expect } from "vitest";
3
+
4
+ const configFile = `
5
+ environment:
6
+ default:
7
+ - name: "TEST"
8
+ value: "test"
9
+
10
+ staging:
11
+ - name: "TEST_2"
12
+ value: "test2"
13
+
14
+ secrets:
15
+ - name: API_KEY
16
+ required:
17
+ - "production"
18
+ `;
19
+
20
+ test("format", async () => {
21
+ const schema = `model Person { fields { name Text } }`;
22
+ const formatted = await format(schema);
23
+ expect(formatted).toEqual(`model Person {
24
+ fields {
25
+ name Text
26
+ }
27
+ }
28
+ `);
29
+ });
30
+
31
+ test("format - invalid schema", async () => {
32
+ const schema = `model Person {`;
33
+ const formatted = await format(schema);
34
+ expect(formatted).toEqual(schema);
35
+ });
36
+
37
+ test("completions", async () => {
38
+ const schema = `model Person {
39
+ fields {
40
+ name Te
41
+ }
42
+ }`;
43
+ const result = await completions({
44
+ schemaFiles: [
45
+ {
46
+ filename: "schema.keel",
47
+ contents: schema,
48
+ },
49
+ ],
50
+ position: {
51
+ filename: "schema.keel",
52
+ line: 3,
53
+ column: 16,
54
+ },
55
+ });
56
+
57
+ expect(result.completions.map((x) => x.label)).toContain("Text");
58
+ });
59
+
60
+ test("completions - multi file", async () => {
61
+ const result = await completions({
62
+ schemaFiles: [
63
+ {
64
+ filename: "schema.keel",
65
+ contents: `
66
+ model Person {
67
+ fields {
68
+ name
69
+ }
70
+ }`,
71
+ },
72
+ {
73
+ filename: "other.keel",
74
+ contents: `
75
+ enum Category {
76
+ Sport
77
+ Finance
78
+ }
79
+ `,
80
+ },
81
+ ],
82
+ position: {
83
+ filename: "schema.keel",
84
+ line: 4,
85
+ column: 10,
86
+ },
87
+ });
88
+
89
+ expect(result.completions.map((x) => x.label)).toContain("Category");
90
+ });
91
+
92
+ test("completions - with config", async () => {
93
+ const result = await completions({
94
+ schemaFiles: [
95
+ {
96
+ filename: "schema.keel",
97
+ contents: `
98
+ model Person {
99
+ @permission(
100
+ expression: ctx.secrets.
101
+ )
102
+ }`,
103
+ },
104
+ ],
105
+ position: {
106
+ filename: "schema.keel",
107
+ line: 4,
108
+ column: 29,
109
+ },
110
+ config: configFile,
111
+ });
112
+
113
+ expect(result.completions.map((x) => x.label)).toContain("API_KEY");
114
+ });
115
+
116
+ test("validate", async () => {
117
+ const schema = `model Person {
118
+ fields {
119
+ name Foo
120
+ }
121
+ }`;
122
+ const { errors } = await validate({
123
+ schemaFiles: [{ filename: "schema.keel", contents: schema }],
124
+ config: configFile,
125
+ });
126
+
127
+ expect(errors[0].message).toEqual("field name has an unsupported type Foo");
128
+ });
129
+
130
+ test("validate - multi file", async () => {
131
+ const schemaA = `model Customer {
132
+ fields {
133
+ orders Order[]
134
+ }
135
+ }`;
136
+ const schemaB = `model Order {
137
+ fields {
138
+ customer Customer
139
+ }
140
+ }`;
141
+ const { errors } = await validate({
142
+ schemaFiles: [
143
+ { filename: "customer.keel", contents: schemaA },
144
+ { filename: "hobby.keel", contents: schemaB },
145
+ ],
146
+ });
147
+
148
+ expect(errors.length).toEqual(0);
149
+ });
150
+
151
+ test("validate - invalid schema", async () => {
152
+ const schema = `model Person {
153
+ fields {`;
154
+ const { errors } = await validate({
155
+ schemaFiles: [{ filename: "schema.keel", contents: schema }],
156
+ config: configFile,
157
+ });
158
+
159
+ expect(errors[0].code).toEqual("E025");
160
+ expect(errors[0].message).toEqual(` unexpected token "<EOF>" (expected "}")`);
161
+ });
162
+
163
+ test("getDefinition", async () => {
164
+ const result = await getDefinition({
165
+ position: {
166
+ line: 7,
167
+ column: 21,
168
+ filename: "myschema.keel",
169
+ },
170
+ schemaFiles: [
171
+ {
172
+ filename: "myschema.keel",
173
+ contents: `
174
+ model Person {
175
+ fields {
176
+ name Text
177
+ }
178
+ operations {
179
+ list getPeople(name)
180
+ }
181
+ }
182
+ `,
183
+ },
184
+ ],
185
+ });
186
+
187
+ expect(result).toEqual({
188
+ function: null,
189
+ schema: {
190
+ filename: "myschema.keel",
191
+ line: 4,
192
+ column: 5,
193
+ },
194
+ });
195
+ });
196
+
197
+ test("getDefinition - no result", async () => {
198
+ const result = await getDefinition({
199
+ position: {
200
+ line: 1,
201
+ column: 1,
202
+ filename: "myschema.keel",
203
+ },
204
+ schemaFiles: [
205
+ {
206
+ filename: "myschema.keel",
207
+ contents: `
208
+ model Person {
209
+ fields {
210
+ name Text
211
+ }
212
+ }
213
+ `,
214
+ },
215
+ ],
216
+ });
217
+
218
+ expect(result).toBeNull();
219
+ });
package/lib/main.go ADDED
@@ -0,0 +1,285 @@
1
+ //go:build wasm
2
+
3
+ package main
4
+
5
+ import (
6
+ "encoding/json"
7
+ "syscall/js"
8
+
9
+ "github.com/teamkeel/keel/config"
10
+ "github.com/teamkeel/keel/schema"
11
+ "github.com/teamkeel/keel/schema/completions"
12
+ "github.com/teamkeel/keel/schema/definitions"
13
+ "github.com/teamkeel/keel/schema/format"
14
+ "github.com/teamkeel/keel/schema/node"
15
+ "github.com/teamkeel/keel/schema/parser"
16
+ "github.com/teamkeel/keel/schema/reader"
17
+ "github.com/teamkeel/keel/schema/validation/errorhandling"
18
+ )
19
+
20
+ func init() {
21
+ // we have to declare our functions in an init func otherwise they aren't
22
+ // available in JS land at the call time.
23
+ js.Global().Set("keel", js.ValueOf(map[string]any{
24
+ "validate": js.FuncOf(validate),
25
+ "format": js.FuncOf(formatSchema),
26
+ "completions": js.FuncOf(provideCompletions),
27
+ "getDefinition": js.FuncOf(getDefinition),
28
+ }))
29
+ }
30
+
31
+ func main() {
32
+ done := make(chan bool)
33
+ <-done
34
+ }
35
+
36
+ // newPromise wraps the provided function in a Javascript Promise
37
+ // and returns that promise. It then either resolves or rejects
38
+ // the promise based on whether fn returns an error or not
39
+ func newPromise(fn func() (any, error)) any {
40
+ handler := js.FuncOf(func(this js.Value, args []js.Value) any {
41
+ resolve := args[0]
42
+ reject := args[1]
43
+
44
+ go func() {
45
+ // handle panics
46
+ defer func() {
47
+ if r := recover(); r != nil {
48
+ msg := "panic"
49
+ switch r.(type) {
50
+ case string:
51
+ msg = r.(string)
52
+ case error:
53
+ e := r.(error)
54
+ msg = e.Error()
55
+ }
56
+ // err should be an instance of `error`, eg `errors.New("some error")`
57
+ errorConstructor := js.Global().Get("Error")
58
+ errorObject := errorConstructor.New(msg)
59
+ reject.Invoke(errorObject)
60
+ }
61
+ }()
62
+
63
+ data, err := fn()
64
+ if err != nil {
65
+ // err should be an instance of `error`, eg `errors.New("some error")`
66
+ errorConstructor := js.Global().Get("Error")
67
+ errorObject := errorConstructor.New(err.Error())
68
+ reject.Invoke(errorObject)
69
+ } else {
70
+ resolve.Invoke(js.ValueOf(data))
71
+ }
72
+ }()
73
+
74
+ return nil
75
+ })
76
+
77
+ promiseConstructor := js.Global().Get("Promise")
78
+ return promiseConstructor.New(handler)
79
+ }
80
+
81
+ // Expected argument to definitions API:
82
+ //
83
+ // {
84
+ // position: {
85
+ // filename: "",
86
+ // line: 1,
87
+ // column: 1,
88
+ // },
89
+ // schemaFiles: [
90
+ // {
91
+ // filename: "",
92
+ // contents: "",
93
+ // },
94
+ // ],
95
+ // config: "",
96
+ // }
97
+ func provideCompletions(this js.Value, args []js.Value) any {
98
+ return newPromise(func() (any, error) {
99
+ positionArg := args[0].Get("position")
100
+ pos := &node.Position{
101
+ Filename: positionArg.Get("filename").String(),
102
+ Line: positionArg.Get("line").Int(),
103
+ Column: positionArg.Get("column").Int(),
104
+ }
105
+
106
+ schemaFilesArg := args[0].Get("schemaFiles")
107
+ schemaFiles := []*reader.SchemaFile{}
108
+ for i := 0; i < schemaFilesArg.Length(); i++ {
109
+ f := schemaFilesArg.Index(i)
110
+ schemaFiles = append(schemaFiles, &reader.SchemaFile{
111
+ FileName: f.Get("filename").String(),
112
+ Contents: f.Get("contents").String(),
113
+ })
114
+ }
115
+
116
+ configSrc := args[0].Get("config")
117
+ var cfg *config.ProjectConfig
118
+ if configSrc.Truthy() {
119
+ // We don't care about errors here, if we can get a config object
120
+ // back we'll use it, if not then we'll run validation without it
121
+ cfg, _ = config.LoadFromBytes([]byte(configSrc.String()))
122
+ }
123
+
124
+ completions := completions.Completions(schemaFiles, pos, cfg)
125
+
126
+ untypedCompletions := toUntypedArray(completions)
127
+
128
+ return map[string]any{
129
+ "completions": js.ValueOf(untypedCompletions),
130
+ }, nil
131
+ })
132
+ }
133
+
134
+ // Expected argument to definitions API:
135
+ //
136
+ // {
137
+ // position: {
138
+ // filename: "",
139
+ // line: 1,
140
+ // column: 1,
141
+ // },
142
+ // schemaFiles: [
143
+ // {
144
+ // filename: "",
145
+ // contents: "",
146
+ // },
147
+ // ],
148
+ // }
149
+ func getDefinition(this js.Value, args []js.Value) any {
150
+ return newPromise(func() (any, error) {
151
+ positionArg := args[0].Get("position")
152
+ pos := definitions.Position{
153
+ Filename: positionArg.Get("filename").String(),
154
+ Line: positionArg.Get("line").Int(),
155
+ Column: positionArg.Get("column").Int(),
156
+ }
157
+
158
+ schemaFilesArg := args[0].Get("schemaFiles")
159
+ schemaFiles := []*reader.SchemaFile{}
160
+ for i := 0; i < schemaFilesArg.Length(); i++ {
161
+ f := schemaFilesArg.Index(i)
162
+ schemaFiles = append(schemaFiles, &reader.SchemaFile{
163
+ FileName: f.Get("filename").String(),
164
+ Contents: f.Get("contents").String(),
165
+ })
166
+ }
167
+
168
+ def := definitions.GetDefinition(schemaFiles, pos)
169
+ if def == nil {
170
+ return nil, nil
171
+ }
172
+
173
+ return toMap(def)
174
+ })
175
+ }
176
+
177
+ func formatSchema(this js.Value, args []js.Value) any {
178
+ return newPromise(func() (any, error) {
179
+ src := args[0].String()
180
+ ast, err := parser.Parse(&reader.SchemaFile{
181
+ FileName: "schema.keel",
182
+ Contents: src,
183
+ })
184
+ if err != nil {
185
+ // if the schema can't be parsed then just return it as-is
186
+ return src, nil
187
+ }
188
+
189
+ return format.Format(ast), nil
190
+ })
191
+ }
192
+
193
+ // Expected argument to validate API:
194
+ //
195
+ // {
196
+ // schemaFiles: [
197
+ // {
198
+ // filename: "",
199
+ // contents: "",
200
+ // },
201
+ // ],
202
+ // config: "<YAML config file>"
203
+ // }
204
+ //
205
+ // The config file source is optional.
206
+ func validate(this js.Value, args []js.Value) any {
207
+ return newPromise(func() (any, error) {
208
+
209
+ schemaFilesArg := args[0].Get("schemaFiles")
210
+ schemaFiles := []reader.SchemaFile{}
211
+ for i := 0; i < schemaFilesArg.Length(); i++ {
212
+ f := schemaFilesArg.Index(i)
213
+ schemaFiles = append(schemaFiles, reader.SchemaFile{
214
+ FileName: f.Get("filename").String(),
215
+ Contents: f.Get("contents").String(),
216
+ })
217
+ }
218
+
219
+ builder := schema.Builder{}
220
+
221
+ configSrc := args[0].Get("config")
222
+ if configSrc.Truthy() {
223
+ // We don't care about errors here, if we can get a config object
224
+ // back we'll use it, if not then we'll run validation without it
225
+ config, _ := config.LoadFromBytes([]byte(configSrc.String()))
226
+ if config != nil {
227
+ builder.Config = config
228
+ }
229
+ }
230
+
231
+ _, err := builder.MakeFromInputs(&reader.Inputs{
232
+ SchemaFiles: schemaFiles,
233
+ })
234
+
235
+ if err != nil {
236
+ errs, ok := err.(*errorhandling.ValidationErrors)
237
+ if !ok {
238
+ return nil, err
239
+ }
240
+
241
+ validationErrors, err := toMap(errs)
242
+ if err != nil {
243
+ return nil, err
244
+ }
245
+
246
+ return validationErrors, nil
247
+ }
248
+
249
+ return map[string]any{
250
+ "errors": []any{},
251
+ }, nil
252
+ })
253
+ }
254
+
255
+ // js.ValueOf can only marshall map[string]any to a JS object
256
+ // so for structs we need to do the struct->json->map[string]any dance
257
+ func toMap(v any) (map[string]any, error) {
258
+ b, err := json.Marshal(v)
259
+ if err != nil {
260
+ return nil, err
261
+ }
262
+
263
+ var res map[string]any
264
+ err = json.Unmarshal(b, &res)
265
+ return res, err
266
+ }
267
+
268
+ func toUntypedArray(items []*completions.CompletionItem) (i []any) {
269
+ for _, item := range items {
270
+ b, err := json.Marshal(item)
271
+
272
+ if err != nil {
273
+ continue
274
+ }
275
+ var res any
276
+ err = json.Unmarshal(b, &res)
277
+
278
+ if err != nil {
279
+ continue
280
+ }
281
+ i = append(i, res)
282
+ }
283
+
284
+ return i
285
+ }