├── .dockerignore
├── .editorconfig
├── .github
├── ISSUE_TEMPLATE
│ ├── config.yml
│ └── internal-issues-only.md
├── pull_request_template.md
└── workflows
│ ├── deno.yml
│ └── docker.yml
├── .gitignore
├── CHANGELOG.md
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── functions
└── .gitkeep
└── src
├── README.md
├── connector.ts
├── entrypoint.sh
├── infer.ts
├── mod.ts
├── schema.ts
├── sdk.ts
├── test
├── classes_test.ts
├── complex_dependency_test.ts
├── conflicting_names_test.ts
├── data
│ ├── classes.ts
│ ├── complex.ts
│ ├── conflicting_names.ts
│ ├── conflicting_names_dep.ts
│ ├── external_dependencies.ts
│ ├── infinite_loop.ts
│ ├── inline_types.ts
│ ├── nullable_types.ts
│ ├── pg_dep.ts
│ ├── program.ts
│ ├── recursive.ts
│ ├── type_parameters.ts
│ ├── validation_algorithm_update.ts
│ └── void_types.ts
├── external_dependencies_test.ts
├── infer_test.ts
├── inline_types_test.ts
├── ndc_schema_test.ts
├── nullable_types_test.ts
├── pg_dep_test.ts
├── prepare_arguments_test.ts
├── recursive_types_test.ts
├── type_parameters_test.ts
├── validation_algorithm_test.ts
└── void_test.ts
└── util.ts
/.dockerignore:
--------------------------------------------------------------------------------
1 | target
2 | Dockerfile
3 | .dockerignore
4 | .gitkeep
5 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: https://EditorConfig.org
2 |
3 | # top-most EditorConfig file
4 | root = true
5 |
6 | [*]
7 | indent_style = space
8 | indent_size = 2
9 | end_of_line = lf
10 | charset = utf-8
11 | trim_trailing_whitespace = true
12 | insert_final_newline = true
13 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: Bugs, Feature Requests, Ideas
4 | url: https://github.com/hasura/graphql-engine/issues/new/choose
5 | about: Use hasura/graphql-engine for issues related to ndc-typescript-deno.
6 | - name: ☁️ Hasura Cloud Help & Support Center
7 | url: https://cloud.hasura.io/support
8 | about: For Issues related to Hasura Cloud and DDN ⚡️
9 | - name: ❓ Get Support from Discord Community
10 | url: https://discord.com/invite/hasura
11 | about: Please ask and answer questions here. 🏥
12 | - name: 🙋♀️ Have a question on how to achieve something with Hasura?
13 | url: https://github.com/hasura/graphql-engine/discussions
14 | about: Start a GitHub discussion
15 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/internal-issues-only.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: New issues are meant to be created by Hasura collaborators only
3 | about: DON'T USE THIS. We request you to use hasura/graphql-engine repo to open new
4 | issues related to ndc-typescript-deno (bugs, feature requests etc).
5 | title: ''
6 | labels: ''
7 | assignees:
8 |
9 | ---
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 |
2 | # TODO: PR TITLE - Please be descriptive!
3 |
4 | TODO: Description
5 |
6 | TODO: Reference issues fixed, etc.
7 |
8 | ## Testing
9 |
10 | TODO: Document steps to test this change or new functionality.
11 |
12 | ## Changelog
13 |
14 | - Add a changelog entry (in the "Changelog entry" section below) if the changes in this PR have any user-facing impact.
15 | - If no changelog is required ignore/remove this section and add a `no-changelog-required` label to the PR.
16 |
17 | ### Type
18 | _(Select only one. In case of multiple, choose the most appropriate)_
19 | - [ ] highlight
20 | - [ ] enhancement
21 | - [ ] bugfix
22 | - [ ] behaviour-change
23 | - [ ] performance-enhancement
24 | - [ ] security-fix
25 |
26 |
27 | ### Changelog entry
28 |
34 |
35 | _Replace with changelog entry_
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/.github/workflows/deno.yml:
--------------------------------------------------------------------------------
1 |
2 | # This workflow will install Deno then run `deno lint` and `deno test`.
3 | # For more information see: https://github.com/denoland/setup-deno
4 |
5 | name: Deno
6 |
7 | on:
8 | push:
9 | branches:
10 | - main
11 | - test-ci/**
12 | tags:
13 | - v**
14 | pull_request:
15 | branches: ["main"]
16 |
17 | permissions:
18 | contents: read
19 |
20 | jobs:
21 | test:
22 | runs-on: ubuntu-latest
23 |
24 | steps:
25 | - name: Setup repo
26 | uses: actions/checkout@v3
27 |
28 | - name: Setup Deno
29 | # uses: denoland/setup-deno@v1
30 | uses: denoland/setup-deno@0df5d9c641efdff149993c321fc27c11c5df8623 # v1.1.3
31 | with:
32 | deno-version: v1.x
33 |
34 | # Uncomment this step to verify the use of 'deno fmt'
35 | # - name: Verify formatting
36 | # run: deno fmt --check
37 | #
38 | # Uncomment this step to run the linter
39 | # - name: Run linter
40 | # run: deno lint
41 |
42 | - name: Run tests
43 | run: deno test -A ./src/test
44 |
--------------------------------------------------------------------------------
/.github/workflows/docker.yml:
--------------------------------------------------------------------------------
1 |
2 | # Example modified from https://docs.github.com/en/actions/publishing-packages/publishing-docker-images
3 | name: Create and publish a Docker image
4 |
5 | # Configures this workflow to run every time a change is pushed to selected tags and branches
6 | on:
7 | push:
8 | branches:
9 | - main
10 | - test-ci/**
11 | tags:
12 | - v**
13 |
14 | # Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds.
15 | env:
16 | REGISTRY: ghcr.io
17 | IMAGE_NAME: ${{ github.repository }}
18 |
19 | # There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu.
20 | jobs:
21 | build-and-push-image:
22 | runs-on: ubuntu-latest
23 | # Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job.
24 | permissions:
25 | contents: read
26 | packages: write
27 | #
28 | steps:
29 | - name: Checkout repository
30 | uses: actions/checkout@v3
31 | # Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here.
32 | - name: Log in to the Container registry
33 | uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
34 | with:
35 | registry: ${{ env.REGISTRY }}
36 | username: ${{ github.actor }}
37 | password: ${{ secrets.GITHUB_TOKEN }}
38 | # This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels.
39 | - name: Extract metadata (tags, labels) for Docker
40 | id: meta
41 | uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
42 | with:
43 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
44 | # This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages.
45 | # It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)" in the README of the `docker/build-push-action` repository.
46 | # It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step.
47 | - name: Build and push Docker image
48 | uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
49 | with:
50 | context: .
51 | push: true
52 | tags: ${{ steps.meta.outputs.tags }}
53 | labels: ${{ steps.meta.outputs.labels }}
54 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | /target
3 | .vscode/
4 | vendor/
5 | scratch/
6 | src/test/vendor
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # TypeScript (Deno) Connector Changelog
2 |
3 | This changelog documents the changes between release versions.
4 |
5 | `main` serves as a place to stash upcoming changes before they have been released under a version tag.
6 |
7 |
8 | ## main
9 |
10 | Changes to be included in the next upcoming release.
11 |
12 | ## v0.24
13 |
14 | * Pinned the Fastify version used to avoid using the latest version which does not work with Deno
15 |
16 | ## v0.23
17 |
18 | Update TS SDK dependency to v1.2.8.
19 |
20 | * Fixed issue where query response format was incorrect when variables were used in the request
21 |
22 | ## v0.22
23 |
24 | Update TS SDK Dependency to v1.2.6.
25 |
26 | * Fixes issue with SERVICE_TOKEN check
27 | * Support for query variables AKA "ForEach"
28 |
29 | ## v0.21
30 |
31 | Support for "nullable" types, for example `string | null`, `string | undefined`, `string | null | undefined`,
32 | and optional object properties.
33 |
34 | PR: https://github.com/hasura/ndc-typescript-deno/pull/82
35 |
36 | ## v0.20
37 |
38 | Improved support for running the connector in Watch mode, where it will auto-restart when changes
39 | to the functions are made.
40 |
41 | * The Docker container is now compatible with watch mode and can be used for local development
42 | * README documentation about watch mode updated to indicate the need to explicitly watch the functions
43 | folder (ie `--watch=./functions`)
44 | * `functions` configuration property is now required
45 | * Type inference is now only done at connector startup, not every time the `/schema` endpoint is called
46 | * Updated TypeScript SDK to v1.2.5
47 |
48 | PR: https://github.com/hasura/ndc-typescript-deno/pull/79
49 |
50 | ## v0.19
51 |
52 | Updating the TS SDK to 1.2.4 and fixing incorrectly required insert_schema field in mutations.
53 |
54 | PR: https://github.com/hasura/ndc-typescript-deno/pull/78
55 |
56 | * Updates SDK
57 |
58 | ## v0.18
59 |
60 | Broadening dependency support, better error logging, and fixing regressions.
61 |
62 | PR: https://github.com/hasura/ndc-typescript-deno/pull/77
63 |
64 | * Ignores TypeScript diagnostic error codes that Deno itself ignores.
65 | * Fixes the target compilation language which got broken in PR #63
66 | * Fatal errors occurring on a particular file are prefixed with FATAL too.
67 |
68 | ## v0.17
69 |
70 | Fixing issue with preVendor default.
71 |
72 | PR: https://github.com/hasura/ndc-typescript-deno/pull/76
73 |
74 | * Was only applying the default during user-interactive config flows
75 |
76 | Fixing NPM inference.
77 |
78 | PR: https://github.com/hasura/ndc-typescript-deno/pull/75
79 |
80 | * Creates node_modules and uses it for inference when vendoring npm dependencies
81 |
82 | ## v0.16
83 |
84 | Prevendoring by default.
85 |
86 | PR: https://github.com/hasura/ndc-typescript-deno/pull/74
87 |
88 | * Using defaults of `{"preVendor": true}` to minimise setup required for development
89 |
90 | ## v0.15
91 |
92 | Updating TypeScript target version from ES2017 to ES2022.
93 |
94 | PR: https://github.com/hasura/ndc-typescript-deno/pull/73
95 |
96 | * Resolves issues with some dependencies such as deno.land/x/postgres@v0.17.0
97 |
98 | ## v0.14.1
99 |
100 | Diff: b1bdc55..17e85d5
101 |
102 | Hotfix for infer bug.
103 |
104 | * Fixes: Infer command logging informational output on stdout
105 |
106 | ## v0.14
107 |
108 | PR: https://github.com/hasura/ndc-typescript-deno/pull/70
109 |
110 | * Fixes: #61 - Use a better check for inline object type detection
111 | * Fixes: #33 - Detect Object and Array scenarios using TS library functions
112 | * Fixes: #45 - Infinite loop for certain function definitions
113 | * Fixes: #51 - Inferring result for non-annotated functions.
114 | * Fixes: #31 - Find a better implementation of is_struct that is type aware
115 | * Fixes: #58 - Generic type parameters are not handled correctly
116 |
117 | ## v0.13
118 |
119 | PR: https://github.com/hasura/ndc-typescript-deno/pull/62
120 |
121 | * Helpful listing of functions/procedures when performing inference
122 |
123 | ## v0.12
124 |
125 | PR: https://github.com/hasura/ndc-typescript-deno/pull/60
126 |
127 | * Use positional names for inline types
128 |
129 | ## v0.11
130 |
131 | PR: https://github.com/hasura/ndc-typescript-deno/pull/59
132 |
133 | * Bugfix: Issue with unused argument being parsed as a file - Prevented invoking on deno.land
134 |
135 | ## v0.10
136 |
137 | PR: https://github.com/hasura/ndc-typescript-deno/pull/57
138 |
139 | * Types names are now preserved when possible
140 | * Depends on the latest version of the TS SDK which no longer needs multiregion functions
141 | * Defines arraybuffer and blob scalars
142 | * Sets MAX_INFERENCE_RECURSION = 20 to break any potential infinite recursion loops
143 | * New tests have been created for refactored inference cases
144 |
145 | ## v0.9
146 |
147 | Full-Stack Typescript!
148 |
149 | * Ported main server from Rust to pure Deno
150 | * Support exposing optional function parameters
151 |
152 | ## v0.8
153 |
154 | Required entrypoint: /functions/index.ts;
155 |
156 | * Entrypoint: /functions/index.ts
157 | * Moving work from entrypoint.sh into Dockerfile to improve hibernation wakeup speed
158 |
159 | ## v0.7
160 |
161 | Startup optimisation.
162 |
163 | * Reusing vendor from inference when running server.ts to avoid downloading dependencies twice
164 |
165 | ## v0.6
166 |
167 | Logging bug fix.
168 |
169 | * Improved entrypoint.sh to have better logging behaviour
170 |
171 | ## v0.5
172 |
173 | Support selecting fields.
174 |
175 | * Query with field selections are now respected
176 | * Errors during function execution are logged from Deno server
177 |
178 | ## v0.4
179 |
180 | Integrated V2 Proxy.
181 |
182 | * Includes a V2 compatibility proxy that can be activated by setting: `ENABLE_V2_COMPATIBILITY=true`
183 | * Fixes bug in `infer.ts` that would throw an error function definitions had no imports: https://github.com/hasura/ndc-typescript-deno/pull/7
184 |
185 | ## v0.3
186 |
187 | Error improvements.
188 |
189 | * Updating SDK
190 | * Correct error reporting from exceptions thrown from user functions
191 |
192 | ## v0.2
193 |
194 | Update to SDK.
195 |
196 | * Starting use of a `CHANGELOG.md` file
197 | * `Deno` is now defined. Users no longer need to defined this value in their functions.
198 | * Inference errors now exit and print in `entrypoint.sh`
199 | * Supports `SERVICE_TOKEN_SECRET`
200 |
201 |
202 | ## v0.1
203 |
204 | Initial release of the connector.
205 |
206 | This connector allows for quick deployment of Typescript functions to Hasura V3 Projects.
207 |
208 | See `README.md` for details.
209 |
210 | Limitations:
211 |
212 | * Server Auth Token support (`SERVICE_TOKEN_SECRET`) is not yet available
213 | * User needs to define `Deno` value in their functions
214 | * Startup inference errors are hidden
215 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM denoland/deno:alpine-1.38.3
2 |
3 | COPY ./src /app
4 | RUN deno cache /app/mod.ts
5 |
6 | COPY ./functions /functions/src
7 |
8 | # Pre-cache inference results and dependencies
9 | RUN PRECACHE_ONLY=true /app/entrypoint.sh
10 |
11 | EXPOSE 8080
12 |
13 | ENTRYPOINT [ "/app/entrypoint.sh" ]
14 |
15 | CMD [ ]
16 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 |
2 | .PHONY: help
3 | help:
4 | @echo Targets:
5 | @cat Makefile | grep '^[a-zA-Z]' | sed 's/^/ * /; s/://'
6 |
7 | test:
8 | deno test -A src/test
9 |
10 | test-and-watch:
11 | deno test -A --watch src/test
12 |
13 | serve:
14 | @echo Note, this requires a config argument - Run the command directly.
15 | deno run -A --watch --check src/mod.ts serve
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ndc-typescript-deno
2 |
3 | 
4 |
5 | The TypeScript (Deno) Connector allows a running connector to be inferred from a TypeScript file (optionally with dependencies).
6 |
7 | 
8 |
9 | Useful Links:
10 |
11 | * [TypeScript Deno Connector on the NDC Hub](https://hasura.io/connectors/typescript-deno)
12 | * [TypeScript Deno Connector on deno.com](https://deno.land/x/hasura_typescript_connector)
13 | * [Hasura V3 Documentation](https://hasura.io/docs/3.0)
14 | * [Hasura CLI](https://hasura.io/docs/3.0/cli/installation/)
15 | * [CLI Connector Plugin](https://hasura.io/docs/latest/hasura-cli/connector-plugin/)
16 | * [Hasura VSCode Extension](https://marketplace.visualstudio.com/items?itemName=HasuraHQ.hasura)
17 | * [Deno](https://deno.com)
18 | * [Native Data Connector Specification](https://hasura.github.io/ndc-spec/)
19 | * [TypeScript NDC SDK](https://github.com/hasura/ndc-sdk-typescript/)
20 |
21 |
22 | ## Overview
23 |
24 | The connector runs in the following manner:
25 |
26 | * TypeScript sources are assembled (with `index.ts` acting as your interface definition)
27 | * Dependencies are fetched
28 | * Inference is performed and made available via the `/schema` endpoint
29 | * Functions are served via the connector protocol
30 |
31 | Note: The Deno runtime is used and this connector assumes that dependencies are specified in accordance with [Deno](https://deno.com) conventions.
32 |
33 |
34 | ## Before you get Started
35 |
36 | It is recommended that you:
37 |
38 | * Install the [Hasura3 CLI](https://hasura.io/docs/3.0/cli/installation/)
39 | * Log in via the CLI
40 | * Install the [connector plugin](https://hasura.io/docs/latest/hasura-cli/connector-plugin/)
41 | * Install [VSCode](https://code.visualstudio.com)
42 | * Install the [Hasura VSCode Extension](https://marketplace.visualstudio.com/items?itemName=HasuraHQ.hasura)
43 | * Optionally install [Deno](https://deno.com)
44 | * Optionally install the [VSCode Deno Extension](https://marketplace.visualstudio.com/items?itemName=denoland.vscode-deno)
45 |
46 | **The last item is currently required for access to the Hasura DDN in order to deploy and manage V3 projects.**
47 |
48 | Once the Hasura DDN is generally available this will no longer be required.
49 |
50 | ## Typescript Functions Format
51 |
52 | Your functions should be organised into a directory with one file acting as the entrypoint.
53 |
54 |
55 | An example TypeScript entrypoint:
56 |
57 | ```typescript
58 |
59 | // functions/index.ts
60 |
61 | import { Hash, encode } from "https://deno.land/x/checksum@1.2.0/mod.ts";
62 |
63 | export function make_bad_password_hash(pw: string): string {
64 | return new Hash("md5").digest(encode(pw)).hex();
65 | }
66 |
67 | /**
68 | * Returns the github bio for the userid provided
69 | *
70 | * @param username Username of the user who's bio will be fetched.
71 | * @returns The github bio for the requested user.
72 | * @pure This function should only query data without making modifications
73 | */
74 | export async function get_github_profile_description(username: string): Promise {
75 | const foo = await fetch(`https://api.github.com/users/${username}`);
76 | const response = await foo.json();
77 | return response.bio;
78 | }
79 |
80 | export function make_array(): Array {
81 | return ['this', 'is', 'an', 'array']
82 | }
83 |
84 | type MyObjectType = {'foo': string, 'baz': Boolean}
85 |
86 | export function make_object(): MyObjectType {
87 | return { 'foo': 'bar', 'baz': true}
88 | }
89 |
90 | export function make_object_array(): Array {
91 | return [make_object(), make_object()]
92 | }
93 |
94 | /**
95 | * @pure
96 | */
97 | export function has_optional_args(a: string, b?: string) {
98 | if(b) {
99 | return `Two args: ${a} ${b}`;
100 | } else {
101 | return `One arg: ${a}`;
102 | }
103 | }
104 | ```
105 |
106 |
107 |
108 | Top level exported function definitions with `pure` tag will be made available as functions,
109 | and others as procedures, which will become queries and mutations respectively.
110 |
111 | * Return types are inferred
112 | * Parameters are inferred and named after their input parameter names.
113 | * Simple scalar, array, and object types should be supported
114 | * Exceptions can be thrown
115 | * Optional parameters will become optional arguments
116 |
117 | Limitations:
118 |
119 | * ~~The `deno vendor` step must be run by hand for local development~~ (vendoring is now automatic by default)
120 | * Functions can be sync, or async, but `Promise`'s can't be nested
121 | * All numbers are exported as `Float`s
122 | * Unrecognised types will become opaque scalars, for example: union types.
123 | * Optional object fields are not currently supported
124 | * Functions can be executed via both the `/query` and `/mutation` endpoints
125 | * Conflicting type names in dependencies will be namespaced with their relative path
126 | * Generic type parameters will be treated as scalars when referenced
127 | * Importing multiple versions of the same package using Deno `npm:` package specifiers does not work properly with type inference (only a single version of the package is imported)
128 |
129 | Please [file an issue](https://github.com/hasura/ndc-typescript-deno/issues/new) for any problems you encounter during usage of this connector.
130 |
131 |
132 | ## Local Development of your Functions
133 |
134 | While you can deploy your functions and have errors returned in the `hasura3 connector` logs,
135 | local development will reward you with much more rapid feedback.
136 |
137 | In order to develop your functions locally the following is the recommended practice:
138 |
139 | * Have a `./functions/` directory in your project
140 | * Create your functions in an `index.ts` file inside the `./functions/` directory
141 | * Create a development config for your connector in `./config.json`:
142 | ```json
143 | {
144 | "functions": "./functions/index.ts",
145 | "vendor": "./vendor",
146 | "schemaMode": "INFER"
147 | }
148 | ```
149 | * Make sure to .gitignore your computed `vendor` files.
150 | * Start the connector
151 | ```sh
152 | deno run -A --watch=./functions --check https://deno.land/x/hasura_typescript_connector/mod.ts serve --configuration ./config.json
153 | ```
154 | * (Optionally) Add a test-suite for your functions. See [Deno Testing Basics](https://docs.deno.com/runtime/manual/basics/testing).
155 |
156 |
157 | ## Local Development of your Functions (Docker)
158 | You can also perform local development with rapid feedback by using the Docker container instead
159 | of `deno run`. You don't need a `config.json` in this case.
160 |
161 | * Have a `./functions/` directory in your project
162 | * Create your functions in an `index.ts` file inside the `./functions/` directory
163 | * Start the connector using Docker:
164 | ```bash
165 | docker run -it --rm -v ./functions:/functions/src -p 8080:8080 -e WATCH=1 ghcr.io/hasura/ndc-typescript-deno:latest
166 | ```
167 |
168 |
169 | ## Config Format
170 |
171 | The configuration object has the following properties:
172 |
173 | ```
174 | functions (string): Location of your functions entrypoint
175 | vendor (string): Location of dependencies vendor folder (optional)
176 | preVendor (boolean): Perform vendoring prior to inference in a sub-process (default: true)
177 | schemaMode (string): INFER the schema from your functions, or READ it from a file. (default: INFER)
178 | schemaLocation (string): Location of your schema file. schemaMode=READ reads the file (required), schemaMode=INFER writes the file (optional)
179 | ```
180 |
181 | NOTE: When deploying the connector with the `connector create` command your config is currently replaced with:
182 |
183 | ```json
184 | {
185 | "functions": "/functions/src/index.ts",
186 | "vendor": "/functions/vendor",
187 | "schemaMode": "READ",
188 | "schemaLocation": "/functions/schema.json"
189 | }
190 | ```
191 |
192 | This means that your functions volume will have to be mounted to `/functions/src`.
193 |
194 | ## Deployment for Hasura Users
195 |
196 | You will need:
197 |
198 | * [V3 CLI](https://hasura.io/docs/3.0/cli/installation/) (With Logged in Session)
199 | * [Connector Plugin](https://hasura.io/docs/latest/hasura-cli/connector-plugin/)
200 | * (Optionally) A value to use with `SERVICE_TOKEN_SECRET`
201 | * a typescript sources directory. E.g. `--volume ./my_functions_directory:/functions`
202 |
203 | Create the connector:
204 |
205 | ```bash
206 | hasura3 connector create my-cool-connector:v1 \
207 | --github-repo-url https://github.com/hasura/ndc-typescript-deno/tree/main \
208 | --config-file <(echo '{}') \
209 | --volume ./functions:/functions \
210 | --env SERVICE_TOKEN_SECRET=MY-SERVICE-TOKEN # (optional)
211 | ```
212 |
213 | *Note: Even though you can use the "main" branch to deploy the latest connector features, see the [Hasura Connector Hub](https://hasura.io/connectors/typescript-deno) for verified release tags*
214 |
215 | Monitor the deployment status by name - This will indicate in-progress, complete, or failed status:
216 |
217 | > hasura3 connector status my-cool-connector:v1
218 |
219 | List all your connectors with their deployed URLs:
220 |
221 | > hasura3 connector list
222 |
223 | View logs from your running connector:
224 |
225 | > hasura3 connector logs my-cool-connector:v1
226 |
227 | ## Usage
228 |
229 | This connector is intended to be used with Hasura v3 projects.
230 |
231 | Find the URL of your connector once deployed:
232 |
233 | > hasura3 connector list
234 |
235 | ```
236 | my-cool-connector:v1 https://connector-9XXX7-hyc5v23h6a-ue.a.run.app active
237 | ```
238 |
239 | In order to use the connector once deployed you will first want to reference the connector in your project metadata:
240 |
241 | ```yaml
242 | kind: "AuthConfig"
243 | allowRoleEmulationFor: "admin"
244 | webhook:
245 | mode: "POST"
246 | webhookUrl: "https://auth.pro.hasura.io/webhook/ddn?role=admin"
247 | ---
248 | kind: DataConnector
249 | version: v1
250 | definition:
251 | name: my_connector
252 | url:
253 | singleUrl: 'https://connector-9XXX7-hyc5v23h6a-ue.a.run.app'
254 | ```
255 |
256 | If you have the [Hasura VSCode Extension](https://marketplace.visualstudio.com/items?itemName=HasuraHQ.hasura) installed
257 | you can run the following code actions:
258 |
259 | * `Hasura: Refresh data source`
260 | * `Hasura: Track all collections / functions ...`
261 |
262 | This will integrate your connector into your Hasura project which can then be deployed or updated using the Hasura3 CLI:
263 |
264 | ```
265 | hasura3 cloud build create --project-id my-project-id --metadata-file metadata.hml
266 | ```
267 |
268 | ## Service Authentication
269 |
270 | If you don't wish to have your connector publically accessible then you must set a service token by specifying the `SERVICE_TOKEN_SECRET` environment variable when creating your connector:
271 |
272 | * `--env SERVICE_TOKEN_SECRET=SUPER_SECRET_TOKEN_XXX123`
273 |
274 | Your Hasura project metadata must then set a matching bearer token:
275 |
276 | ```yaml
277 | kind: DataConnector
278 | version: v1
279 | definition:
280 | name: my_connector
281 | url:
282 | singleUrl: 'https://connector-9XXX7-hyc5v23h6a-ue.a.run.app'
283 | headers:
284 | Authorization:
285 | value: "Bearer SUPER_SECRET_TOKEN_XXX123"
286 | ```
287 |
288 | While you can specify the token inline as above, it is recommended to use the Hasura secrets functionality for this purpose:
289 |
290 | ```yaml
291 | kind: DataConnector
292 | version: v1
293 | definition:
294 | name: my_connector
295 | url:
296 | singleUrl: 'https://connector-9XXX7-hyc5v23h6a-ue.a.run.app'
297 | headers:
298 | Authorization:
299 | valueFromSecret: BEARER_TOKEN_SECRET
300 | ```
301 |
302 | NOTE: This secret should contain the `Bearer ` prefix.
303 |
304 | ## Debugging Issues
305 |
306 | Errors may arise from any of the following:
307 |
308 | * Dependency errors in your functions
309 | * Type errors in your functions
310 | * Implementation errors in your functions
311 | * Invalid connector configuration
312 | * Invalid project metadata
313 | * Connector Deployment Failure
314 | * Misconfigured project authentication
315 | * Misconfigured service authentication
316 | * Insufficient query permissions
317 | * Invalid queries
318 | * Unanticipated bug in connector implementation
319 |
320 | For a bottom-up debugging approach:
321 |
322 | * First check your functions:
323 | * Run `deno check` on your functions to determine if there are any obvious errors
324 | * Write a `deno test` harness to ensure that your functions are correctly implemented
325 | * Then check your connector:
326 | * Check that the connector deployed successfully with `hasura3 connector status my-cool-connector:v1`
327 | * Check the build/runtime logs of your connector with `hasura3 connector logs my-cool-connector:v1`
328 | * Then check your project:
329 | * Ensure that your metadata and project build were successful
330 | * Then check end-to-end integration:
331 | * Run test queries and view the connector logs to ensure that your queries are propagating correctly
332 |
333 |
334 | ## Development
335 |
336 | For contribution to this connector you will want to have the following dependencies:
337 |
338 | * [Deno](https://deno.com)
339 | * (Optionally) [Docker](https://www.docker.com)
340 |
341 | In order to perform local development on this codebase:
342 |
343 | * Check out the repository: `git clone https://github.com/hasura/ndc-typescript-deno.git`
344 | * This assumes that you will be testing against function in `./functions`
345 | * Serve your functions with `deno run -A --watch=./functions --check ./src/mod.ts serve --configuration <(echo '{"functions": "./functions/index.ts", "vendor": "./vendor", "schemaMode": "INFER"}')`
346 | * The connector should now be running on localhost:8100 and respond to any changes to the your functions and the connector source
347 | * Use the `hasura3` tunnel commands to reference this connector from a Hasura Cloud project
348 |
349 | If you are fixing a bug, then please consider adding a test case to `./src/test/data`.
350 |
351 | ## Support & Troubleshooting
352 |
353 | The documentation and community will help you troubleshoot most issues.
354 | If you have encountered a bug or need to get in touch with us, you can contact us using one of the following channels:
355 |
356 | - Support & feedback: [Discord](https://discord.gg/hasura)
357 | - Issue & bug tracking: [GitHub issues](https://github.com/hasura/graphql-engine/issues)
358 | - Follow product updates: [@HasuraHQ](https://twitter.com/hasurahq)
359 | - Talk to us on our [website chat](https://hasura.io)
360 |
--------------------------------------------------------------------------------
/functions/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hasura/ndc-typescript-deno/97b218d1c8cb57e184f6d0f56a6f720da857aae6/functions/.gitkeep
--------------------------------------------------------------------------------
/src/README.md:
--------------------------------------------------------------------------------
1 | # Hasura NDC TypeScript (Deno) Connector
2 |
3 | 
4 |
5 | The TypeScript (Deno) Connector allows a running connector to be inferred from a TypeScript file (optionally with dependencies).
6 |
7 | 
8 |
9 | ## [Github Repository](https://github.com/hasura/ndc-typescript-deno)
10 |
11 | See the [Github Repo](https://github.com/hasura/ndc-typescript-deno) for additional details.
12 |
13 | ## Quickstart
14 |
15 | Once your project is set up, run locally with:
16 |
17 | ```
18 | > cat config.json
19 | {
20 | "functions": "./functions/index.ts",
21 | "vendor": "./vendor",
22 | "schemaMode": "INFER"
23 | }
24 |
25 | > deno run -A --watch=./functions --check https://deno.land/x/hasura_typescript_connector/mod.ts serve --configuration ./config.json
26 | ```
27 |
28 | ## TypeScript Functions Format
29 |
30 | Your functions should be organised into a directory with one file acting as the entrypoint.
31 |
32 | An example Typescript entrypoint:
33 |
34 | ```typescript
35 |
36 | // functions/index.ts
37 |
38 | import { Hash, encode } from "https://deno.land/x/checksum@1.2.0/mod.ts";
39 |
40 | export function make_bad_password_hash(pw: string): string {
41 | return new Hash("md5").digest(encode(pw)).hex();
42 | }
43 |
44 | /**
45 | * Returns the github bio for the userid provided
46 | *
47 | * @param username Username of the user who's bio will be fetched.
48 | * @returns The github bio for the requested user.
49 | * @pure This function should only query data without making modifications
50 | */
51 | export async function get_github_profile_description(username: string): Promise {
52 | const foo = await fetch(`https://api.github.com/users/${username}`);
53 | const response = await foo.json();
54 | return response.bio;
55 | }
56 |
57 | export function make_array(): Array {
58 | return ['this', 'is', 'an', 'array']
59 | }
60 |
61 | type MyObjectType = {'foo': string, 'baz': Boolean}
62 |
63 | export function make_object(): MyObjectType {
64 | return { 'foo': 'bar', 'baz': true}
65 | }
66 |
67 | export function make_object_array(): Array {
68 | return [make_object(), make_object()]
69 | }
70 |
71 | /**
72 | * @pure
73 | */
74 | export function has_optional_args(a: string, b?: string) {
75 | if(b) {
76 | return `Two args: ${a} ${b}`;
77 | } else {
78 | return `One arg: ${a}`;
79 | }
80 | }
81 | ```
82 |
--------------------------------------------------------------------------------
/src/connector.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Implementation of the Connector interface for Deno connector.
3 | * Using https://github.com/hasura/ndc-qdrant/blob/main/src/index.ts as an example.
4 | */
5 |
6 | import { FunctionDefinitions, get_ndc_schema, NullOrUndefinability, ObjectTypeDefinitions, ProgramSchema, TypeDefinition } from "./schema.ts";
7 | import { inferProgramSchema, Struct, } from "./infer.ts";
8 | import { resolve } from "https://deno.land/std@0.208.0/path/mod.ts";
9 | import { JSONSchemaObject } from "npm:@json-schema-tools/meta-schema";
10 | import { isArray, unreachable } from "./util.ts";
11 |
12 | import { sdk } from './sdk.ts';
13 |
14 | export type State = {
15 | functions: RuntimeFunctions
16 | }
17 |
18 | export type RuntimeFunctions = {
19 | // deno-lint-ignore ban-types
20 | [function_name: string]: Function
21 | }
22 |
23 | export interface RawConfiguration {
24 | functions: string,
25 | port?: number, // Included only for punning Connector.start()
26 | hostname?: string, // Included only for punning Connector.start()
27 | schemaMode?: 'READ' | 'INFER',
28 | schemaLocation?: string,
29 | vendor?: string,
30 | preVendor?: boolean,
31 | }
32 |
33 | export const RAW_CONFIGURATION_SCHEMA: JSONSchemaObject = {
34 | description: 'Typescript (Deno) Connector Configuration',
35 | type: 'object',
36 | required: [ 'functions' ],
37 | properties:
38 | {
39 | functions: {
40 | description: 'Location of your functions entrypoint (default: ./functions/index.ts)',
41 | type: 'string'
42 | },
43 | vendor: {
44 | description: 'Location of dependencies vendor folder (optional)',
45 | type: 'string'
46 | },
47 | preVendor: {
48 | description: 'Perform vendoring prior to inference in a sub-process (default: true)',
49 | type: 'boolean'
50 | },
51 | schemaMode: {
52 | description: 'INFER the schema from your functions, or READ it from a file.',
53 | type: "string",
54 | enum: ["READ", "INFER"]
55 | },
56 | schemaLocation: {
57 | description: 'Location of your schema file. schemaMode=READ reads the file, schemaMode=INFER writes the file (optional)',
58 | type: 'string'
59 | },
60 | }
61 | };
62 |
63 | type Configuration = {
64 | inferenceConfig: InferenceConfig,
65 | programSchema: ProgramSchema,
66 | }
67 |
68 | type InferenceConfig = {
69 | functions: string,
70 | schemaMode: 'READ' | 'INFER',
71 | schemaLocation?: string,
72 | vendorDir: string,
73 | preVendor: boolean,
74 | }
75 |
76 | export const CAPABILITIES_RESPONSE: sdk.CapabilitiesResponse = {
77 | versions: "^0.1.0",
78 | capabilities: {
79 | query: {
80 | variables: {}
81 | },
82 | },
83 | };
84 |
85 | type Payload = {
86 | function: string,
87 | args: Struct
88 | }
89 |
90 |
91 | ///////////////////// Helper Functions /////////////////////
92 |
93 |
94 | /**
95 | * Performs analysis on the supplied program.
96 | * Expects that if there are dependencies then they will have been vendored.
97 | *
98 | * @param cmdObj
99 | * @returns Schema and argument position information
100 | */
101 | export function getProgramSchema(cmdObj: InferenceConfig): ProgramSchema {
102 | switch(cmdObj.schemaMode) {
103 | /**
104 | * The READ option is available in case the user wants to pre-cache their schema during development.
105 | */
106 | case 'READ': {
107 | if(!cmdObj.schemaLocation) {
108 | throw new Error('--schema-location is required if using --schema-mode READ');
109 | }
110 | console.error(`Reading existing schema: ${cmdObj.schemaLocation}`);
111 | const bytes = Deno.readFileSync(cmdObj.schemaLocation);
112 | const decoder = new TextDecoder("utf-8");
113 | const decoded = decoder.decode(bytes);
114 | return JSON.parse(decoded);
115 | }
116 | case 'INFER': {
117 | console.error(`Inferring schema with map location ${cmdObj.vendorDir}`);
118 | const programSchema = inferProgramSchema(cmdObj.functions, cmdObj.vendorDir, cmdObj.preVendor);
119 | const schemaLocation = cmdObj.schemaLocation;
120 | if(schemaLocation) {
121 | console.error(`Writing schema to ${cmdObj.schemaLocation}`);
122 | const infoString = JSON.stringify(programSchema);
123 | // NOTE: Using sync functions should be ok since they're run on startup.
124 | Deno.writeTextFileSync(schemaLocation, infoString);
125 | }
126 | return programSchema;
127 | }
128 | default:
129 | throw new Error('Invalid schema-mode. Use READ or INFER.');
130 | }
131 | }
132 |
133 | /**
134 | * Performs invocation of the requested function.
135 | * Assembles the arguments into the correct order.
136 | * This doesn't catch any exceptions.
137 | *
138 | * @param functions
139 | * @param function_definitions
140 | * @param payload
141 | * @returns the result of invocation with no wrapper
142 | */
143 | async function invoke(function_name: string, args: Record, functions: RuntimeFunctions, program_schema: ProgramSchema): Promise {
144 | const func = functions[function_name];
145 | const prepared_args = prepare_arguments(function_name, args, program_schema.functions, program_schema.object_types);
146 |
147 | try {
148 | const result = func.apply(undefined, prepared_args);
149 | // Resolve the result if it is a promise
150 | if (typeof result === "object" && 'then' in result && typeof result.then === "function") {
151 | return await result;
152 | }
153 | return result;
154 | } catch (e) {
155 | throw new sdk.InternalServerError(`Error encountered when invoking function ${function_name}`, { message: e.message, stack: e.stack });
156 | }
157 | }
158 |
159 | /**
160 | * This takes argument position information and a payload of function
161 | * and named arguments and returns the correctly ordered arguments ready to be applied.
162 | *
163 | * @param function_definitions
164 | * @param payload
165 | * @returns An array of the function's arguments in the definition order
166 | */
167 | export function prepare_arguments(function_name: string, args: Record, function_definitions: FunctionDefinitions, object_type_definitions: ObjectTypeDefinitions): unknown[] {
168 | const function_definition = function_definitions[function_name];
169 |
170 | if(!function_definition) {
171 | throw new sdk.InternalServerError(`Couldn't find function ${function_name} in schema.`);
172 | }
173 |
174 | return function_definition.arguments
175 | .map(argDef => coerce_argument_value(args[argDef.argument_name], argDef.type, [argDef.argument_name], object_type_definitions));
176 | }
177 |
178 | function coerce_argument_value(value: unknown, type: TypeDefinition, value_path: string[], object_type_definitions: ObjectTypeDefinitions): unknown {
179 | switch (type.type) {
180 | case "array":
181 | if (!isArray(value))
182 | throw new sdk.BadRequest(`Unexpected value in function arguments. Expected an array at '${value_path.join(".")}'.`);
183 | return value.map((element, index) => coerce_argument_value(element, type.element_type, [...value_path, `[${index}]`], object_type_definitions))
184 |
185 | case "nullable":
186 | if (value === null) {
187 | return type.null_or_undefinability == NullOrUndefinability.AcceptsUndefinedOnly
188 | ? undefined
189 | : null;
190 | } else if (value === undefined) {
191 | return type.null_or_undefinability == NullOrUndefinability.AcceptsNullOnly
192 | ? null
193 | : undefined;
194 | } else {
195 | return coerce_argument_value(value, type.underlying_type, value_path, object_type_definitions)
196 | }
197 | case "named":
198 | if (type.kind === "scalar") {
199 | // Scalars are currently treated as opaque values, which is a bit dodgy
200 | return value;
201 | } else {
202 | const object_type_definition = object_type_definitions[type.name];
203 | if (!object_type_definition)
204 | throw new sdk.InternalServerError(`Couldn't find object type '${type.name}' in the schema`);
205 | if (value === null || typeof value !== "object") {
206 | throw new sdk.BadRequest(`Unexpected value in function arguments. Expected an object at '${value_path.join(".")}'.`);
207 | }
208 | return Object.fromEntries(object_type_definition.properties.map(property_definition => {
209 | const prop_value = (value as Record)[property_definition.property_name];
210 | return [property_definition.property_name, coerce_argument_value(prop_value, property_definition.type, [...value_path, property_definition.property_name], object_type_definitions)]
211 | }));
212 | }
213 | default:
214 | return unreachable(type["type"]);
215 | }
216 | }
217 |
218 | // TODO: https://github.com/hasura/ndc-typescript-deno/issues/26 Do deeper field recursion once that's available
219 | function pruneFields(func: string, fields: Struct | null | undefined, result: unknown): unknown {
220 | // This seems like a bug to request {} fields when expecting a scalar response...
221 | // File with engine?
222 | if(!fields || Object.keys(fields).length == 0) {
223 | // TODO: https://github.com/hasura/ndc-typescript-deno/issues/21 How to log with SDK?
224 | console.error(`Warning: No fields present in query for function ${func}.`);
225 | return result;
226 | }
227 |
228 | const response: Struct = {};
229 |
230 | if (result === null || Array.isArray(result) || typeof result !== "object") {
231 | throw new sdk.InternalServerError(`Function '${func}' did not return an object when expected to`);
232 | }
233 |
234 | for(const [k,v] of Object.entries(fields)) {
235 | switch(v.type) {
236 | case 'column':
237 | response[k] = (result as Record)[v.column] ?? null; // Coalesce undefined into null to ensure we always have a value for a requested column
238 | break;
239 | default:
240 | console.error(`Function ${func} field of type ${v.type} is not supported.`);
241 | }
242 | }
243 |
244 | return response;
245 | }
246 |
247 | async function query(
248 | configuration: Configuration,
249 | state: State,
250 | functionName: string,
251 | requestArgs: Struct,
252 | requestFields: { [k: string]: sdk.Field; } | null,
253 | ): Promise {
254 | const result = await invoke(functionName, requestArgs, state.functions, configuration.programSchema);
255 | return pruneFields(functionName, requestFields, result);
256 | }
257 |
258 | function resolveArguments(
259 | func: string,
260 | requestArgs: Struct,
261 | requestVariables: { [k: string]: unknown },
262 | ): Struct {
263 | const args = Object.fromEntries(Object.entries(requestArgs).map(([k,v], _i) => {
264 | const t = v.type;
265 | switch(t) {
266 | case 'literal':
267 | return [k, v.value];
268 | case 'variable': {
269 | if(!(v.name in requestVariables)) {
270 | throw new Error(`Variable ${v.name} not found for function ${func}`);
271 | }
272 | const value = requestVariables[v.name];
273 | return [k, value];
274 | }
275 | default:
276 | return unreachable(t)
277 | }
278 | }));
279 | return args;
280 | }
281 |
282 | /**
283 | * See https://github.com/hasura/ndc-sdk-typescript for information on these interfaces.
284 | */
285 | export const connector: sdk.Connector = {
286 | async try_init_state(
287 | config: Configuration,
288 | _metrics: unknown
289 | ): Promise {
290 | const functionsArg = config.inferenceConfig.functions;
291 | const functionsURL = `file://${functionsArg}`; // NOTE: This is required to run directly from deno.land.
292 | const functions = await import(functionsURL);
293 | return {
294 | functions
295 | }
296 | },
297 |
298 | get_capabilities(_: Configuration): sdk.CapabilitiesResponse {
299 | return CAPABILITIES_RESPONSE;
300 | },
301 |
302 | get_raw_configuration_schema(): JSONSchemaObject {
303 | return RAW_CONFIGURATION_SCHEMA;
304 | },
305 |
306 | make_empty_configuration(): RawConfiguration {
307 | const conf: RawConfiguration = {
308 | functions: './functions/index.ts',
309 | vendor: './vendor'
310 | };
311 | return conf;
312 | },
313 |
314 | // TODO: https://github.com/hasura/ndc-typescript-deno/issues/27 Make this add in the defaults
315 | update_configuration(configuration: RawConfiguration): Promise {
316 | return Promise.resolve(configuration);
317 | },
318 |
319 | validate_raw_configuration(configuration: RawConfiguration): Promise {
320 | if (configuration.functions.trim() === "") {
321 | throw new sdk.BadRequest("'functions' must be set to the location of the TypeScript file that contains your functions")
322 | }
323 | if (configuration.schemaMode === "READ" && !configuration.schemaLocation) {
324 | throw new sdk.BadRequest("'schemaLocation' must be set if 'schemaMode' is READ");
325 | }
326 | const inferenceConfig: InferenceConfig = {
327 | functions: resolve(configuration.functions),
328 | schemaMode: configuration.schemaMode ?? "INFER",
329 | preVendor: configuration.preVendor ?? true,
330 | schemaLocation: configuration.schemaLocation,
331 | vendorDir: resolve(configuration.vendor || "./vendor"),
332 | };
333 | const programSchema = getProgramSchema(inferenceConfig);
334 | return Promise.resolve({
335 | inferenceConfig,
336 | programSchema
337 | });
338 | },
339 |
340 | get_schema(config: Configuration): Promise {
341 | return Promise.resolve(get_ndc_schema(config.programSchema));
342 | },
343 |
344 | // TODO: https://github.com/hasura/ndc-typescript-deno/issues/28 What do we want explain to do in this scenario?
345 | explain(
346 | _configuration: Configuration,
347 | _: State,
348 | _request: sdk.QueryRequest
349 | ): Promise {
350 | throw new Error('Implementation of `explain` pending.');
351 | },
352 |
353 | // NOTE: query and mutation both make all functions available and discrimination is performed by the schema
354 | async query(
355 | configuration: Configuration,
356 | state: State,
357 | request: sdk.QueryRequest
358 | ): Promise {
359 |
360 | const rowSets: sdk.RowSet[] = [];
361 | for(const variables of request.variables ?? [{}]) {
362 | const args = resolveArguments(request.collection, request.arguments, variables);
363 | const result = await query(configuration, state, request.collection, args, request.query.fields ?? null);
364 | rowSets.push({
365 | aggregates: {},
366 | rows: [
367 | { '__value': result }
368 | ]
369 | });
370 | }
371 |
372 | return rowSets;
373 | },
374 |
375 | async mutation(
376 | configuration: Configuration,
377 | state: State,
378 | request: sdk.MutationRequest
379 | ): Promise {
380 | const results: Array = [];
381 | for(const op of request.operations) {
382 | switch(op.type) {
383 | case 'procedure': {
384 | const result = await query(configuration, state, op.name, op.arguments, op.fields ?? null);
385 | results.push({
386 | affected_rows: 1,
387 | returning: [{
388 | '__value': result
389 | }]
390 | });
391 | break;
392 | }
393 | default:
394 | throw new Error(`Mutation type ${op.type} not supported.`);
395 | }
396 | }
397 | return {
398 | operation_results: results
399 | }
400 | },
401 |
402 | // If the connector starts successfully it should be healthy
403 | health_check(_: Configuration, __: State): Promise {
404 | return Promise.resolve(undefined);
405 | },
406 |
407 | // TODO: https://github.com/hasura/ndc-typescript-deno/issues/29 https://qdrant.github.io/qdrant/redoc/index.html#tag/service/operation/metrics
408 | fetch_metrics(_: Configuration, __: State): Promise {
409 | return Promise.resolve(undefined);
410 | },
411 | };
412 |
--------------------------------------------------------------------------------
/src/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ####
4 | # This script serves as the entrypoint for the Dockerfile
5 | # It is used both during the build phase (with PRECACHE_ONLY=true), and during the run phase.
6 | # This could be split into two scripts easily enough if that is required.
7 | ####
8 |
9 | set -e
10 |
11 | cd /functions
12 |
13 | if [ ! -f ./src/index.ts ]
14 | then
15 | echo "No /functions/src/index.ts found - Please place your functions in /functions/src"
16 | exit 0
17 | fi
18 |
19 | if [ -d vendor ]
20 | then
21 | echo "found existing vendor results"
22 | else
23 | deno vendor --node-modules-dir --vendor /functions/vendor -f ./src/index.ts
24 | fi
25 |
26 | if [ -f schema.json ]
27 | then
28 | echo "found existing inference results"
29 | else
30 | deno run \
31 | --allow-env --allow-sys --allow-read --allow-net --allow-write \
32 | /app/mod.ts infer \
33 | --vendor /functions/vendor \
34 | ./src/index.ts >schema.json
35 | fi
36 |
37 | if [ "$PRECACHE_ONLY" ]
38 | then
39 | echo "Thanks for running pre-caching - Please come again soon!"
40 | exit 0
41 | fi
42 |
43 | if [[ "$WATCH" == "1" || "$WATCH" == "true" ]]
44 | then
45 | DENO_PARAMS="--watch=/functions/src --no-clear-screen"
46 | echo '{"functions": "/functions/src/index.ts", "vendor": "/functions/vendor", "preVendor": true, "schemaMode": "INFER" }' \
47 | > /etc/connector-config.json
48 |
49 | else
50 | DENO_PARAMS=""
51 | echo '{"functions": "/functions/src/index.ts", "vendor": "/functions/vendor", "preVendor": false, "schemaMode": "READ", "schemaLocation": "/functions/schema.json"}' \
52 | > /etc/connector-config.json
53 | fi
54 |
55 | deno run \
56 | --allow-run --allow-net --allow-read --allow-write --allow-env --allow-sys \
57 | $DENO_PARAMS \
58 | /app/mod.ts serve \
59 | --port 8080 \
60 | --configuration /etc/connector-config.json
61 |
--------------------------------------------------------------------------------
/src/infer.ts:
--------------------------------------------------------------------------------
1 |
2 | /**
3 | * This module provides the inference implementation for the connector.
4 | * It relies on the Typescript compiler to perform the heavy lifting.
5 | *
6 | * The exported function that is intended for use is `inferProgramSchema`.
7 | *
8 | * Dependencies are required to be vendored before invocation.
9 | */
10 |
11 | import ts, { FunctionDeclaration, StringLiteralLike } from "npm:typescript@5.1.6";
12 | import { resolve, dirname } from "https://deno.land/std@0.208.0/path/mod.ts";
13 | import { existsSync } from "https://deno.land/std@0.208.0/fs/mod.ts";
14 | import { mapObject } from "./util.ts";
15 | import { FunctionDefinitions, FunctionNdcKind, NullOrUndefinability, ObjectTypeDefinitions, ProgramSchema, ScalarTypeDefinitions, TypeDefinition, listing } from "./schema.ts";
16 |
17 | export type Struct = Record;
18 |
19 | const scalar_mappings: {[key: string]: string} = {
20 | "string": "String",
21 | "bool": "Boolean",
22 | "boolean": "Boolean",
23 | "number": "Float",
24 | "arraybuffer": "ArrayBuffer", // Treat ArrayBuffer as scalar since it shouldn't recur
25 | "blob": "Blob", // Treat ArrayBuffer as scalar since it shouldn't recur
26 | // "void": "Void", // Void type can be included to permit void types as scalars
27 | };
28 |
29 | // TODO: https://github.com/hasura/ndc-typescript-deno/issues/21 Use standard logging from SDK
30 | const LOG_LEVEL = Deno.env.get("LOG_LEVEL") || "INFO";
31 | const DEBUG = LOG_LEVEL == 'DEBUG';
32 | const MAX_INFERENCE_RECURSION = 20; // Better to abort than get into an infinite loop, this could be increased if required.
33 |
34 | function gql_name(n: string): string {
35 | // Construct a GraphQL complient name: https://spec.graphql.org/draft/#sec-Type-Name-Introspection
36 | // Check if this is actually required.
37 | return n.replace(/^[^a-zA-Z]/, '').replace(/[^0-9a-zA-Z]/g,'_');
38 | }
39 |
40 | function qualify_type_name(root_file: string, t: any, name: string): string {
41 | let symbol = t.getSymbol();
42 |
43 | if (!symbol) {
44 | try {
45 | symbol = t.types[0].getSymbol();
46 | } catch (e) {
47 | throw new Error(`Couldn't find symbol for type ${name}`, { cause: e });
48 | }
49 | }
50 |
51 | const locations = symbol.declarations.map((d: ts.Declaration) => d.getSourceFile());
52 | for(const f of locations) {
53 | const where = f.fileName;
54 | const short = where.replace(dirname(root_file) + '/','').replace(/\.ts$/, '');
55 |
56 | // If the type is present in the entrypoint, don't qualify the name
57 | // If it is under the entrypoint's directory qualify with the subpath
58 | // Otherwise, use the minimum ancestor of the type's location to ensure non-conflict
59 | if(root_file == where) {
60 | return name;
61 | } else if (short.length < where.length) {
62 | return `${gql_name(short)}_${name}`;
63 | } else {
64 | throw new Error(`Unsupported location for type ${name} in ${where}`);
65 | }
66 | }
67 |
68 | throw new Error(`Couldn't find any declarations for type ${name}`);
69 | }
70 |
71 | function validate_type(root_file: string, checker: ts.TypeChecker, object_type_definitions: ObjectTypeDefinitions, scalar_type_definitions: ScalarTypeDefinitions, name: string, ty: any, depth: number): TypeDefinition {
72 | const type_str = checker.typeToString(ty);
73 | const type_name = ty.symbol?.escapedName || ty.intrinsicName || 'unknown_type';
74 | const type_name_lower: string = type_name.toLowerCase();
75 |
76 | if(depth > MAX_INFERENCE_RECURSION) {
77 | throw_error(`Schema inference validation exceeded depth ${MAX_INFERENCE_RECURSION} for type ${type_str}`);
78 | }
79 |
80 | // PROMISE
81 | // TODO: https://github.com/hasura/ndc-typescript-deno/issues/32 There is no recursion that resolves inner promises.
82 | // Nested promises should be resolved in the function definition.
83 | // TODO: promises should not be allowed in parameters
84 | if (type_name == "Promise") {
85 | const inner_type = ty.resolvedTypeArguments[0];
86 | const inner_type_result = validate_type(root_file, checker, object_type_definitions, scalar_type_definitions, name, inner_type, depth + 1);
87 | return inner_type_result;
88 | }
89 |
90 | // ARRAY
91 | if (checker.isArrayType(ty)) {
92 | const inner_type = ty.resolvedTypeArguments[0];
93 | const inner_type_result = validate_type(root_file, checker, object_type_definitions, scalar_type_definitions, `Array_of_${name}`, inner_type, depth + 1);
94 | return { type: 'array', element_type: inner_type_result };
95 | }
96 |
97 | // NULL OR UNDEFINED TYPES (x | null, x | undefined, x | null | undefined)
98 | const not_nullable_result = unwrap_nullable_type(ty);
99 | if (not_nullable_result !== null) {
100 | const [not_nullable_type, null_or_undefinability] = not_nullable_result;
101 | const not_nullable_type_result = validate_type(root_file, checker, object_type_definitions, scalar_type_definitions, `Array_of_${name}`, not_nullable_type, depth + 1);
102 | return { type: "nullable", underlying_type: not_nullable_type_result, null_or_undefinability: null_or_undefinability }
103 | }
104 |
105 | // Named SCALAR
106 | if (scalar_mappings[type_name_lower]) {
107 | const type_name_gql = scalar_mappings[type_name_lower];
108 | scalar_type_definitions[type_name_gql] = {};
109 | return { type: 'named', name: type_name_gql, kind: "scalar" };
110 | }
111 |
112 | // OBJECT
113 | // TODO: https://github.com/hasura/ndc-typescript-deno/issues/33 There should be a library function that allows us to check this case
114 | const info = get_object_type_info(root_file, checker, ty, name);
115 | if (info) {
116 | const type_str_qualified = info.type_name;
117 |
118 | // Shortcut recursion if the type has already been named
119 | if (object_type_definitions[type_str_qualified]) {
120 | return { type: 'named', name: type_str_qualified, kind: "object" };
121 | }
122 |
123 | object_type_definitions[type_str_qualified] = { properties: [] }; // Break infinite recursion
124 |
125 | const properties = Array.from(info.members, ([property_name, property_type]) => {
126 | const property_type_validated = validate_type(root_file, checker, object_type_definitions, scalar_type_definitions, `${name}_field_${property_name}`, property_type, depth + 1);
127 | return { property_name, type: property_type_validated };
128 | });
129 |
130 | object_type_definitions[type_str_qualified] = { properties }
131 |
132 | return { type: 'named', name: type_str_qualified, kind: "object" }
133 | }
134 |
135 | // TODO: We could potentially support classes, but only as return types, not as function arguments
136 | if ((ty.objectFlags & ts.ObjectFlags.Class) !== 0) {
137 | console.error(`class types are not supported: ${name}`);
138 | throw_error('validate_type failed');
139 | }
140 |
141 | if (ty === checker.getVoidType()) {
142 | console.error(`void functions are not supported: ${name}`);
143 | throw_error('validate_type failed');
144 | }
145 |
146 | // UNHANDLED: Assume that the type is a scalar
147 | console.error(`Unable to validate type of ${name}: ${type_str} (${type_name}). Assuming that it is a scalar type.`);
148 | scalar_type_definitions[name] = {};
149 | return { type: 'named', name, kind: "scalar" };
150 | }
151 |
152 | /**
153 | * Executes `deno vendor` in a subprocess as a conveneience.
154 | *
155 | * @param vendorPath
156 | * @param filename
157 | */
158 | function pre_vendor(vendorPath: string, filename: string) {
159 | // Exampe taken from:
160 | // https://docs.deno.com/runtime/tutorials/subprocess
161 | const deno_exec_path = Deno.execPath();
162 | const vendor_args = [ "vendor", "--node-modules-dir", "--output", vendorPath, "--force", filename ];
163 |
164 | console.error(`Vendoring dependencies: ${[deno_exec_path, ...vendor_args].join(" ")}`);
165 |
166 | const vendor_command = new Deno.Command(deno_exec_path, { args: vendor_args });
167 | const { code, stdout, stderr } = vendor_command.outputSync();
168 |
169 | if(code !== 0) {
170 | console.error(`Error: Got code ${code} during deno vendor operation.`)
171 | console.error(`stdout: ${new TextDecoder().decode(stdout)}`);
172 | console.error(`stderr: ${new TextDecoder().decode(stderr)}`);
173 | throw_error('pre_vendor failed');
174 | }
175 | }
176 |
177 | function throw_error(message: string): never {
178 | throw new Error(message);
179 | }
180 |
181 | /**
182 | * Returns the flags associated with a type.
183 | */
184 | function which_flags(flags_enum: Record, value: number): string[] {
185 | return Object
186 | .keys(flags_enum)
187 | .flatMap(k => {
188 | const k_int = parseInt(k);
189 | return isNaN(k_int)
190 | ? []
191 | : (value & k_int) !== 0
192 | ? [flags_enum[k] as string]
193 | : []
194 | });
195 | }
196 |
197 | type ObjectTypeInfo = {
198 | // The qualified name of the type
199 | type_name: string,
200 | // Parameter types used with this type in positional order
201 | generic_parameter_types: readonly ts.Type[]
202 | // The member properties of the object type. The types are
203 | // concrete types after type parameter resolution
204 | members: Map
205 | }
206 |
207 | function get_members(checker: ts.TypeChecker, ty: ts.Type, member_names: string[]) {
208 | return new Map(
209 | member_names.map(name => [name, checker.getTypeOfSymbol(checker.getPropertyOfType(ty, name)!)])
210 | )
211 | }
212 |
213 | function get_object_type_info(root_file: string, checker: ts.TypeChecker, ty: any, contextual_name: string): ObjectTypeInfo | null {
214 | // Anonymous object type - this covers:
215 | // - {a: number, b: string}
216 | // - type Bar = { test: string }
217 | // - type GenericBar = { data: T }
218 | if ((ty.objectFlags & ts.ObjectFlags.Anonymous) !== 0) {
219 | const members =
220 | ty.aliasTypeArguments !== undefined
221 | ? ty.target.members
222 | : ty.members;
223 | return {
224 | type_name: qualify_type_name(root_file, ty, ty.aliasSymbol ? checker.typeToString(ty) : contextual_name),
225 | generic_parameter_types: ty.aliasTypeArguments ?? [],
226 | members: get_members(checker, ty, Array.from(members.keys())),
227 | }
228 | }
229 | // Interface type - this covers:
230 | // interface IThing { test: string }
231 | else if ((ty.objectFlags & ts.ObjectFlags.Interface) !== 0) {
232 | return {
233 | type_name: ty.symbol.escapedName,
234 | generic_parameter_types: [],
235 | members: get_members(checker, ty, Array.from(ty.members.keys())),
236 | }
237 | }
238 | // Generic interface type - this covers:
239 | // interface IGenericThing { data: T }
240 | else if ((ty.objectFlags & ts.ObjectFlags.Reference) !== 0 && (ty.target.objectFlags & ts.ObjectFlags.Interface) !== 0 && checker.isArrayType(ty) == false && ty.symbol.escapedName !== "Promise") {
241 | return {
242 | type_name: ty.symbol.escapedName,
243 | generic_parameter_types: ty.typeArguments,
244 | members: get_members(checker, ty, Array.from(ty.target.members.keys())),
245 | }
246 | }
247 | // Intersection type - this covers:
248 | // - { num: number } & Bar
249 | // - type IntersectionObject = { wow: string } & Bar
250 | // - type GenericIntersectionObject = { data: T } & Bar
251 | else if ((ty.flags & ts.TypeFlags.Intersection) !== 0) {
252 | return {
253 | type_name: qualify_type_name(root_file, ty, ty.aliasSymbol ? checker.typeToString(ty) : contextual_name),
254 | generic_parameter_types: ty.aliasTypeArguments ?? [],
255 | members: new Map(ty.resolvedProperties.map((symbol: ts.Symbol) => [symbol.name, checker.getTypeOfSymbol(symbol)])),
256 | }
257 | }
258 |
259 | return null;
260 | }
261 |
262 | function unwrap_nullable_type(ty: ts.Type): [ts.Type, NullOrUndefinability] | null {
263 | if (!ty.isUnion()) return null;
264 |
265 | const isNullable = ty.types.find(is_null_type) !== undefined;
266 | const isUndefined = ty.types.find(is_undefined_type) !== undefined;
267 | const nullOrUndefinability =
268 | isNullable
269 | ? ( isUndefined
270 | ? NullOrUndefinability.AcceptsEither
271 | : NullOrUndefinability.AcceptsNullOnly
272 | )
273 | : ( isUndefined
274 | ? NullOrUndefinability.AcceptsUndefinedOnly
275 | : null
276 | );
277 |
278 | const typesWithoutNullAndUndefined = ty.types
279 | .filter(t => !is_null_type(t) && !is_undefined_type(t));
280 |
281 | return typesWithoutNullAndUndefined.length === 1 && nullOrUndefinability
282 | ? [typesWithoutNullAndUndefined[0], nullOrUndefinability]
283 | : null;
284 | }
285 |
286 | function is_null_type(ty: ts.Type): boolean {
287 | return (ty.flags & ts.TypeFlags.Null) !== 0;
288 | }
289 | function is_undefined_type(ty: ts.Type): boolean {
290 | return (ty.flags & ts.TypeFlags.Undefined) !== 0;
291 | }
292 |
293 | export function inferProgramSchema(filename: string, vendorPath: string, perform_vendor: boolean): ProgramSchema {
294 | // TODO: https://github.com/hasura/ndc-typescript-deno/issues/27 This should have already been established upstream
295 | const importMapPath = `${vendorPath}/import_map.json`;
296 | let pathsMap: {[key: string]: Array} = {};
297 |
298 | // NOTE: We can't just move this inside the else branch of the exists importMap check
299 | // Since the dependencies may change when updating your functions.
300 | if(perform_vendor) {
301 | pre_vendor(vendorPath, filename);
302 | }
303 |
304 | if (existsSync(importMapPath)) {
305 | const importString = Deno.readTextFileSync(importMapPath);
306 | const vendorMap = JSON.parse(importString);
307 | pathsMap = mapObject(vendorMap.imports, (k: string, v: string) => {
308 | if(/\.ts$/.test(k)) {
309 | return [k, [ v.replace(/./, vendorPath) ]];
310 | } else {
311 | return [k.replace(/$/,'*'), [ v.replace(/./, vendorPath).replace(/$/, '*') ]];
312 | }
313 | });
314 | } else {
315 | // NOTE: We allow the import map to be optional but dependency lookup will fail if it was required.
316 | console.error(`Couldn't find import map: ${importMapPath}`);
317 | }
318 |
319 | const deno_d_ts = Deno.makeTempFileSync({ suffix: ".d.ts" });
320 | Deno.writeTextFileSync(deno_d_ts, `
321 | /**
322 | * This module exists to be included as a library by the typescript compiler in infer.ts.
323 | * The reason for this is that the user is likely to use the Deno dev tools when developing their functions.
324 | * And they will have Deno in scope.
325 | * This ensures that these references will typecheck correctly in infer.ts.
326 | */
327 |
328 | export {};
329 |
330 | declare global {
331 | var Deno: any
332 | }
333 | `);
334 |
335 | const compilerOptions: ts.CompilerOptions = {
336 | // This should match the version targeted in the deno version that is being used.
337 | target: ts.ScriptTarget.ES2022,
338 | module: ts.ModuleKind.CommonJS,
339 | noImplicitAny: true,
340 | // NOTE: We just declare Deno globally as any in order to allow users to omit it's declaration in their function files
341 | // This should ideally use the real deno type definitions.
342 | lib: ['lib.d.ts', 'lib.es2022.d.ts', resolve(deno_d_ts)],
343 | allowJs: true,
344 | allowImportingTsExtensions: true,
345 | noEmit: true,
346 | baseUrl: '.',
347 | paths: pathsMap,
348 | strictNullChecks: true,
349 | };
350 |
351 | const host = ts.createCompilerHost(compilerOptions);
352 | host.resolveModuleNameLiterals = (moduleLiterals: StringLiteralLike[], containingFile: string): ts.ResolvedModuleWithFailedLookupLocations[] => {
353 | return moduleLiterals.map(moduleName => {
354 | let moduleNameToResolve = moduleName.text;
355 | // If this looks like a Deno "npm:pkgName[@version][/path]" module import, extract the node module
356 | // name and resolve that instead. So long as we've done a deno vendor with --node-modules-dir
357 | // then we'll have a node_modules directory that the standard TypeScript module resolution
358 | // process can locate the npm package in by its name
359 | const npmDepMatch = /^npm:(?(?:@.+?\/)?[^/\n]+?)(?:@.+)?(?:\/.+)?$/.exec(moduleName.text);
360 | if (npmDepMatch) {
361 | moduleNameToResolve = npmDepMatch.groups?.pkgName!;
362 | }
363 |
364 | return ts.resolveModuleName(moduleNameToResolve, containingFile, compilerOptions, { fileExists: host.fileExists, readFile: host.readFile });
365 | })
366 | }
367 |
368 | const program = ts.createProgram([filename], compilerOptions, host);
369 |
370 | Deno.removeSync(deno_d_ts);
371 |
372 | // These diagnostic codes are ignored because Deno ignores them
373 | // See: https://github.com/denoland/deno/blob/bf42467e215b20b36ec6b4bf30212e4beb2dd01f/cli/tsc/99_main_compiler.js#L441
374 | const ignoredDiagnosticCodes = [1452, 2306, 2688, 2792, 5009, 5055, 5070, 7016];
375 | const diagnostics = ts.getPreEmitDiagnostics(program);
376 |
377 | // https://github.com/Microsoft/TypeScript/wiki/Using-the-Compiler-API
378 | if (diagnostics.length) {
379 | let fatal = 0;
380 | console.error(`There were ${diagnostics.length} diagnostic errors.`);
381 | diagnostics.filter(d => !ignoredDiagnosticCodes.includes(d.code)).forEach(diagnostic => {
382 | if (diagnostic.file) {
383 | let errorPrefix = "";
384 | const isFatal = !resolve(diagnostic.file.fileName).startsWith(vendorPath)
385 | if (isFatal) {
386 | fatal++;
387 | errorPrefix = "FATAL: "
388 | }
389 | const { line, character } = ts.getLineAndCharacterOfPosition(diagnostic.file, diagnostic.start!);
390 | const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
391 | console.error(`${errorPrefix}${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`);
392 | } else {
393 | console.error(`FATAL: ${ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n")}`);
394 | fatal++;
395 | }
396 | });
397 |
398 | if(fatal > 0) {
399 | throw_error(`Fatal errors: ${fatal}`);
400 | }
401 | }
402 |
403 | const checker = program.getTypeChecker();
404 |
405 | const object_type_definitions: ObjectTypeDefinitions = {};
406 | const function_definitions: FunctionDefinitions = {};
407 | const scalar_type_definitions: ScalarTypeDefinitions = {};
408 |
409 | function isExported(node: FunctionDeclaration): boolean {
410 | for(const mod of node.modifiers || []) {
411 | if(mod.kind == ts.SyntaxKind.ExportKeyword) {
412 | return true;
413 | }
414 | }
415 | return false;
416 | }
417 |
418 | for (const src of program.getSourceFiles()) {
419 | if (src.isDeclarationFile) {
420 | if(DEBUG) {
421 | console.error(`Skipping analysis of declaration source: ${src.fileName}`);
422 | }
423 | continue;
424 | }
425 |
426 | if (resolve(src.fileName) != resolve(filename)) {
427 | if(DEBUG) {
428 | console.error(`Skipping analysis of source with resolve inconsistency: ${src.fileName}`);
429 | }
430 | continue;
431 | }
432 |
433 | const root_file = resolve(filename);
434 |
435 | ts.forEachChild(src, (node: ts.Node) => {
436 | if (ts.isFunctionDeclaration(node)) {
437 | const fn_sym = checker.getSymbolAtLocation(node.name!)!;
438 | const fn_name = fn_sym.escapedName;
439 |
440 | if(!isExported(node)) {
441 | console.error(`Skipping non-exported function: ${fn_name}`);
442 | return;
443 | }
444 |
445 | const fn_type = checker.getTypeOfSymbolAtLocation(fn_sym, fn_sym.valueDeclaration!);
446 | const fn_desc = ts.displayPartsToString(fn_sym.getDocumentationComment(checker)).trim();
447 | const fn_tags = fn_sym.getJsDocTags();
448 | const fn_pure = !!(fn_tags.find((e) => e.name == 'pure'));
449 |
450 | const call = fn_type.getCallSignatures()[0]!;
451 | const result_type = call.getReturnType();
452 | const result_type_name = `${fn_name}_output`;
453 |
454 | const result_type_validated = validate_type(root_file, checker, object_type_definitions, scalar_type_definitions, result_type_name, result_type, 0);
455 |
456 | const function_arguments = call.parameters.map(param => {
457 | const param_name = param.getName();
458 | const param_desc = ts.displayPartsToString(param.getDocumentationComment(checker)).trim();
459 | const param_type = checker.getTypeOfSymbolAtLocation(param, param.valueDeclaration!);
460 | // TODO: https://github.com/hasura/ndc-typescript-deno/issues/34 Use the user's given type name if one exists.
461 | const type_name = `${fn_name}_arguments_${param_name}`;
462 | const param_type_validated = validate_type(root_file, checker, object_type_definitions, scalar_type_definitions, type_name, param_type, 0); // E.g. `bio_arguments_username`
463 | const description = param_desc ? param_desc : null
464 |
465 | return {
466 | argument_name: param_name,
467 | description,
468 | type: param_type_validated,
469 | }
470 | });
471 |
472 | function_definitions[node.name!.text] = {
473 | ndc_kind: fn_pure ? FunctionNdcKind.Function : FunctionNdcKind.Procedure,
474 | description: fn_desc ? fn_desc : null,
475 | arguments: function_arguments,
476 | result_type: result_type_validated
477 | };
478 | }
479 | });
480 |
481 | }
482 |
483 | const result = {
484 | functions: function_definitions,
485 | object_types: object_type_definitions,
486 | scalar_types: scalar_type_definitions,
487 | }
488 |
489 | listing(FunctionNdcKind.Function, result.functions)
490 | listing(FunctionNdcKind.Procedure, result.functions)
491 |
492 | return result;
493 | }
494 |
--------------------------------------------------------------------------------
/src/mod.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Typescript entrypoint for running the connector.
3 | */
4 |
5 | // NOTE: Ensure that sdk matches version in connector.ts
6 | import * as commander from 'npm:commander@11.0.0';
7 | import { inferProgramSchema } from './infer.ts'
8 | import { connector } from './connector.ts'
9 | import { sdk } from './sdk.ts';
10 |
11 | const inferCommand = new commander.Command("infer")
12 | .argument('', 'TypeScript source entrypoint')
13 | .option('-v, --vendor ', 'Vendor location (optional)')
14 | .action((entrypoint, cmdObj, _command) => {
15 | const output = inferProgramSchema(entrypoint, cmdObj.vendor, cmdObj.preVendor);
16 | console.log(JSON.stringify(output));
17 | });
18 |
19 | const program = new commander.Command('typescript-connector');
20 |
21 | program.addCommand(sdk.get_serve_command(connector));
22 | program.addCommand(sdk.get_serve_configuration_command(connector));
23 | program.addCommand(inferCommand);
24 |
25 | // The commander library expects node style arguments that have
26 | // 'node' and the entrypoint as the first two arguments.
27 | // The node_style_args array makes Deno.args compatible.
28 | // The import.meta.url is used instead of a file, since it may be invoked from deno.land
29 | const node_style_args = [Deno.execPath(), import.meta.url, ...Deno.args];
30 |
31 | program.parseAsync(node_style_args).catch(console.error);
32 |
--------------------------------------------------------------------------------
/src/schema.ts:
--------------------------------------------------------------------------------
1 | import { sdk } from './sdk.ts';
2 | import { mapObjectValues, unreachable } from "./util.ts";
3 |
4 | export type ProgramSchema = {
5 | functions: FunctionDefinitions
6 | object_types: ObjectTypeDefinitions
7 | scalar_types: ScalarTypeDefinitions
8 | }
9 |
10 | export type FunctionDefinitions = {
11 | [function_name: string]: FunctionDefinition
12 | }
13 |
14 | export type FunctionDefinition = {
15 | ndc_kind: FunctionNdcKind
16 | description: string | null,
17 | arguments: ArgumentDefinition[] // Function arguments are ordered
18 | result_type: TypeDefinition
19 | }
20 |
21 | export enum FunctionNdcKind {
22 | Function = "Function",
23 | Procedure = "Procedure"
24 | }
25 |
26 | export type ArgumentDefinition = {
27 | argument_name: string,
28 | description: string | null,
29 | type: TypeDefinition
30 | }
31 |
32 | export type ObjectTypeDefinitions = {
33 | [object_type_name: string]: ObjectTypeDefinition
34 | }
35 |
36 | export type ObjectTypeDefinition = {
37 | properties: ObjectPropertyDefinition[]
38 | }
39 |
40 | export type ObjectPropertyDefinition = {
41 | property_name: string,
42 | type: TypeDefinition,
43 | }
44 |
45 | export type ScalarTypeDefinitions = {
46 | [scalar_type_name: string]: ScalarTypeDefinition
47 | }
48 |
49 | export type ScalarTypeDefinition = Record // Empty object, for now
50 |
51 | export type TypeDefinition = ArrayTypeDefinition | NullableTypeDefinition | NamedTypeDefinition
52 |
53 | export type ArrayTypeDefinition = {
54 | type: "array"
55 | element_type: TypeDefinition
56 | }
57 |
58 | export type NullableTypeDefinition = {
59 | type: "nullable",
60 | null_or_undefinability: NullOrUndefinability
61 | underlying_type: TypeDefinition
62 | }
63 |
64 | export type NamedTypeDefinition = {
65 | type: "named"
66 | name: string
67 | kind: "scalar" | "object"
68 | }
69 |
70 | export enum NullOrUndefinability {
71 | AcceptsNullOnly = "AcceptsNullOnly",
72 | AcceptsUndefinedOnly = "AcceptsUndefinedOnly",
73 | AcceptsEither = "AcceptsEither",
74 | }
75 |
76 | export function get_ndc_schema(programInfo: ProgramSchema): sdk.SchemaResponse {
77 | const functions = Object.entries(programInfo.functions);
78 |
79 | const object_types = mapObjectValues(programInfo.object_types, obj_def => {
80 | return {
81 | fields: Object.fromEntries(obj_def.properties.map(prop_def => [prop_def.property_name, { type: convert_type_definition_to_sdk_type(prop_def.type)}]))
82 | }
83 | });
84 |
85 | const scalar_types = mapObjectValues(programInfo.scalar_types, _scalar_def => {
86 | return {
87 | aggregate_functions: {},
88 | comparison_operators: {},
89 | }
90 | })
91 |
92 | return {
93 | functions: functions
94 | .filter(([_, def]) => def.ndc_kind === FunctionNdcKind.Function)
95 | .map(([name, def]) => convert_function_definition_to_sdk_schema_type(name, def)),
96 | procedures: functions
97 | .filter(([_, def]) => def.ndc_kind === FunctionNdcKind.Procedure)
98 | .map(([name, def]) => convert_function_definition_to_sdk_schema_type(name, def)),
99 | collections: [],
100 | object_types,
101 | scalar_types,
102 | }
103 | }
104 |
105 | function convert_type_definition_to_sdk_type(typeDef: TypeDefinition): sdk.Type {
106 | switch (typeDef.type) {
107 | case "array": return { type: "array", element_type: convert_type_definition_to_sdk_type(typeDef.element_type) }
108 | case "nullable": return { type: "nullable", underlying_type: convert_type_definition_to_sdk_type(typeDef.underlying_type) }
109 | case "named": return { type: "named", name: typeDef.name }
110 | default: return unreachable(typeDef["type"])
111 | }
112 | }
113 |
114 | function convert_function_definition_to_sdk_schema_type(function_name: string, definition: FunctionDefinition): sdk.FunctionInfo | sdk.ProcedureInfo {
115 | const args =
116 | definition.arguments
117 | .map(arg_def =>
118 | [ arg_def.argument_name,
119 | {
120 | type: convert_type_definition_to_sdk_type(arg_def.type),
121 | ...(arg_def.description ? { description: arg_def.description } : {}),
122 | }
123 | ]
124 | );
125 |
126 | return {
127 | name: function_name,
128 | arguments: Object.fromEntries(args),
129 | result_type: convert_type_definition_to_sdk_type(definition.result_type),
130 | ...(definition.description ? { description: definition.description } : {}),
131 | }
132 | }
133 |
134 | /**
135 | * Logs simple listing of functions/procedures on stderr.
136 | *
137 | * @param prompt
138 | * @param functionDefinitions
139 | * @param info
140 | */
141 | export function listing(functionNdcKind: FunctionNdcKind, functionDefinitions: FunctionDefinitions) {
142 | const functions = Object.entries(functionDefinitions).filter(([_, def]) => def.ndc_kind === functionNdcKind);
143 | if (functions.length > 0) {
144 | console.error(``);
145 | console.error(`${functionNdcKind}s:`)
146 | for (const [function_name, function_definition] of functions) {
147 | const args = function_definition.arguments.join(', ');
148 | console.error(`* ${function_name}(${args})`);
149 | }
150 | console.error(``);
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/src/sdk.ts:
--------------------------------------------------------------------------------
1 | // This pins the fastify version (transitively used by the ndc-sdk-typescript)
2 | // because 4.26.0 introduces a deno-incompatible change
3 | import fastify from "npm:fastify@4.25.2"
4 | // Have this dependency defined in one place
5 | export * as sdk from 'npm:@hasura/ndc-sdk-typescript@1.2.8';
6 |
--------------------------------------------------------------------------------
/src/test/classes_test.ts:
--------------------------------------------------------------------------------
1 |
2 | import * as test from "https://deno.land/std@0.208.0/assert/mod.ts";
3 | import * as path from "https://deno.land/std@0.208.0/path/mod.ts";
4 | import * as infer from '../infer.ts';
5 |
6 | // Classes are currently not supoported and should throw an error
7 | Deno.test("Classes", () => {
8 | const program_path = path.fromFileUrl(import.meta.resolve('./data/classes.ts'));
9 | const vendor_path = path.fromFileUrl(import.meta.resolve('./vendor'));
10 | test.assertThrows(() => {
11 | infer.inferProgramSchema(program_path, vendor_path, false);
12 | })
13 | });
14 |
--------------------------------------------------------------------------------
/src/test/complex_dependency_test.ts:
--------------------------------------------------------------------------------
1 |
2 | import * as test from "https://deno.land/std@0.208.0/assert/mod.ts";
3 | import * as path from "https://deno.land/std@0.208.0/path/mod.ts";
4 | import * as infer from '../infer.ts';
5 | import { FunctionNdcKind } from "../schema.ts";
6 |
7 | // This program omits its return type and it is inferred via the 'fetch' dependency.
8 | Deno.test("Inference on Dependency", () => {
9 | const program_path = path.fromFileUrl(import.meta.resolve('./data/infinite_loop.ts'));
10 | const vendor_path = path.fromFileUrl(import.meta.resolve('./vendor'));
11 | const program_schema = infer.inferProgramSchema(program_path, vendor_path, false);
12 | test.assertEquals(program_schema.functions, {
13 | infinite_loop: {
14 | ndc_kind: FunctionNdcKind.Procedure,
15 | description: null,
16 | arguments: [],
17 | result_type: { type: "named", kind: "object", name: "Response" }
18 | }
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/src/test/conflicting_names_test.ts:
--------------------------------------------------------------------------------
1 |
2 | import * as test from "https://deno.land/std@0.208.0/assert/mod.ts";
3 | import * as path from "https://deno.land/std@0.208.0/path/mod.ts";
4 | import * as infer from '../infer.ts';
5 | import { FunctionNdcKind } from "../schema.ts";
6 |
7 | Deno.test("Conflicting Type Names in Imports", () => {
8 | const program_path = path.fromFileUrl(import.meta.resolve('./data/conflicting_names.ts'));
9 | const vendor_path = path.fromFileUrl(import.meta.resolve('./vendor'));
10 |
11 | const program_schema = infer.inferProgramSchema(program_path, vendor_path, false);
12 |
13 | test.assertEquals(program_schema, {
14 | functions: {
15 | "foo": {
16 | ndc_kind: FunctionNdcKind.Procedure,
17 | description: null,
18 | arguments: [],
19 | result_type: {
20 | name: "Foo",
21 | kind: "object",
22 | type: "named",
23 | },
24 | }
25 | },
26 | object_types: {
27 | Foo: {
28 | properties: [
29 | {
30 | property_name: "x",
31 | type: {
32 | name: "Boolean",
33 | kind: "scalar",
34 | type: "named",
35 | },
36 | },
37 | {
38 | property_name: "y",
39 | type: {
40 | name: "conflicting_names_dep_Foo",
41 | kind: "object",
42 | type: "named",
43 | },
44 | },
45 | ]
46 | },
47 | conflicting_names_dep_Foo: {
48 | properties: [
49 | {
50 | property_name: "a",
51 | type: {
52 | name: "String",
53 | kind: "scalar",
54 | type: "named",
55 | },
56 | },
57 | {
58 | property_name: "b",
59 | type: {
60 | name: "Float",
61 | kind: "scalar",
62 | type: "named",
63 | },
64 | },
65 | ]
66 | },
67 | },
68 | scalar_types: {
69 | Boolean: {},
70 | Float: {},
71 | String: {},
72 | }
73 | })
74 | });
75 |
--------------------------------------------------------------------------------
/src/test/data/classes.ts:
--------------------------------------------------------------------------------
1 |
2 |
3 | class MyClass {
4 | }
5 |
6 | export function bar(
7 | clazz: MyClass
8 | ): string {
9 | return 'hello';
10 | }
11 |
--------------------------------------------------------------------------------
/src/test/data/complex.ts:
--------------------------------------------------------------------------------
1 |
2 | import { Hash, encode } from "https://deno.land/x/checksum@1.2.0/mod.ts";
3 |
4 | type Result = {
5 | num: number,
6 | str: string,
7 | bod: string
8 | }
9 |
10 | export async function complex(a: number, b: number, c?: string): Promise {
11 | const num = a + b;
12 | const msg = `${c || 'Addition'}: ${num}`;
13 | const hash = new Hash("md5").digest(encode(msg)).hex();
14 | const str = `Yo: ${msg} - ${hash}`;
15 | const res = await fetch('https://httpbin.org/get');
16 | const bod = await res.text();
17 |
18 | return {
19 | num,
20 | str,
21 | bod
22 | }
23 | }
--------------------------------------------------------------------------------
/src/test/data/conflicting_names.ts:
--------------------------------------------------------------------------------
1 |
2 | import * as dep from './conflicting_names_dep.ts';
3 |
4 | type Foo = {
5 | x: boolean,
6 | y: dep.Foo
7 | }
8 |
9 | export function foo(): Foo {
10 | return {
11 | x: true,
12 | y: {
13 | a: 'hello',
14 | b: 33
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/test/data/conflicting_names_dep.ts:
--------------------------------------------------------------------------------
1 |
2 | export type Foo = {
3 | a: string,
4 | b: number
5 | }
6 |
--------------------------------------------------------------------------------
/src/test/data/external_dependencies.ts:
--------------------------------------------------------------------------------
1 |
2 | import * as base64 from "https://denopkg.com/chiefbiiko/base64@v0.2.1/mod.ts";
3 | import * as emojify from "npm:node-emoji@2.1";
4 |
5 | export function test_deps(s: string): string {
6 | const b64 = base64.fromUint8Array(new TextEncoder().encode(s));
7 | const emo = emojify.emojify(":t-rex: :heart: NPM");
8 | return `${b64} ${emo}`;
9 | }
--------------------------------------------------------------------------------
/src/test/data/infinite_loop.ts:
--------------------------------------------------------------------------------
1 |
2 | // Infinite loop void bug: https://github.com/hasura/ndc-typescript-deno/issues/45
3 |
4 | export async function infinite_loop() {
5 | const contents = await fetch('https://www.google.com')
6 | return contents;
7 | }
8 |
--------------------------------------------------------------------------------
/src/test/data/inline_types.ts:
--------------------------------------------------------------------------------
1 |
2 |
3 | export function bar(x: {a: number, b: string}): string {
4 | return 'hello';
5 | }
6 |
--------------------------------------------------------------------------------
/src/test/data/nullable_types.ts:
--------------------------------------------------------------------------------
1 |
2 | type MyObject = {
3 | string: string,
4 | nullableString: string | null,
5 | optionalString?: string
6 | undefinedString: string | undefined
7 | nullOrUndefinedString: string | undefined | null
8 | }
9 |
10 | export function test(
11 | myObject: MyObject,
12 | nullableParam: string | null,
13 | undefinedParam: string | undefined,
14 | nullOrUndefinedParam: string | undefined | null,
15 | unionWithNull: string | number | null,
16 | optionalParam?: string
17 | ): string | null {
18 | return "test"
19 | }
20 |
--------------------------------------------------------------------------------
/src/test/data/pg_dep.ts:
--------------------------------------------------------------------------------
1 | import { Client } from "https://deno.land/x/postgres@v0.17.0/mod.ts";
2 |
3 | const dbConfig = {
4 | user: "aaysha",
5 | hostname: "asdfasdfasd.us-west-2.aws.neon.tech",
6 | port: 5432,
7 | password: "asdfasdasdf",
8 | database: "asdfasdfasdf",
9 | ssl: true,
10 | sslmode: "require",
11 | };
12 |
13 | export async function insert_user(
14 | user_name: string,
15 | ): Promise<
16 | { id: string; name: string; created_at: string } | { message: string } | {
17 | error: string;
18 | } | {}
19 | > {
20 | const client = new Client(dbConfig);
21 |
22 | try {
23 | await client.connect();
24 |
25 | const result = await client.queryObject({
26 | text: `INSERT INTO users(name) VALUES ('${user_name}') RETURNING *`,
27 | });
28 |
29 | if (result && result.rows.length > 0 && result.rows[0]) {
30 | return result.rows[0];
31 | } else {
32 | return { message: "Insert Failed" };
33 | }
34 | } catch (error) {
35 | console.error("Error:", error);
36 | return { error: "Error: " + error.message };
37 | } finally {
38 | await client.end();
39 | }
40 | }
41 |
42 | export async function insert_todos(
43 | user_id: string,
44 | todo: string
45 | ): Promise<
46 | { id: string; user_id: string; todo: string; created_at: string } | { message: string } | {
47 | error: string;
48 | } | {}
49 | > {
50 | const client = new Client(dbConfig);
51 |
52 | try {
53 | await client.connect();
54 |
55 |
56 | // Check if the user exists in the users table
57 |
58 | const userExistsQuery = await client.queryObject({
59 | text: `SELECT id FROM users where id =${user_id}`
60 | })
61 |
62 | if (userExistsQuery.rows.length === 0) {
63 | return { message: "User not found. Insert Failed" };
64 | }
65 | const result = await client.queryObject({
66 | text: `INSERT INTO todos(user_id,todo) VALUES ('${user_id}','${todo}') RETURNING *`,
67 | });
68 |
69 | if (result && result.rows.length > 0 && result.rows[0]) {
70 | return result.rows[0];
71 | } else {
72 | return { message: "Insert Failed" };
73 | }
74 | } catch (error) {
75 | console.error("Error:", error);
76 | return { error: "Error: " + error.message };
77 | } finally {
78 | await client.end();
79 | }
80 | }
81 |
82 |
83 | export async function delete_todos(
84 | todo_id: string
85 | ){
86 | const client = new Client(dbConfig);
87 | try{
88 | await client.connect();
89 |
90 | const result = await client.queryObject({ text: `DELETE FROM todos WHERE id =${todo_id}`})
91 | if(result.rowCount===1){
92 | return `Deleted todo with id= ${todo_id} sucesssfully`
93 | }else{
94 | return "Deletion unsucessfull"
95 | }
96 | }catch(error){
97 | return "Error : "+ error.message
98 | }finally{
99 | client.end();
100 | }
101 |
102 | }
--------------------------------------------------------------------------------
/src/test/data/program.ts:
--------------------------------------------------------------------------------
1 |
2 | function non_exported() {
3 | }
4 |
5 | export function hello(): string {
6 | return 'hello world';
7 | }
8 |
9 | export function add(a: number, b: number): number {
10 | return a + b;
11 | }
--------------------------------------------------------------------------------
/src/test/data/recursive.ts:
--------------------------------------------------------------------------------
1 |
2 |
3 | // Named types should prevent infinite recursion in schema inference
4 |
5 | type Foo = {
6 | a: number,
7 | b: Array
8 | }
9 |
10 | export function bar(): Foo {
11 | return {
12 | a: 1,
13 | b: [{
14 | a: 2,
15 | b: []
16 | }]
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/test/data/type_parameters.ts:
--------------------------------------------------------------------------------
1 |
2 |
3 | // This tests that type parameters for object don't short circuit with a scalar
4 |
5 | type Foo = {
6 | a: number,
7 | b: string
8 | }
9 |
10 | type Bar = {
11 | x: number,
12 | y: X
13 | }
14 |
15 | // Foo and Bar should both have `object_types` defined.
16 | export function bar(): Bar {
17 | return {
18 | x: 1,
19 | y: {
20 | a: 2,
21 | b: 'hello'
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/test/data/validation_algorithm_update.ts:
--------------------------------------------------------------------------------
1 |
2 | // Placeholder index.ts to allow CI to run against functions volume
3 |
4 | type Bar = {
5 | test: string
6 | }
7 |
8 | type GenericBar = {
9 | data: T
10 | }
11 |
12 | interface IThing {
13 | prop: string
14 | }
15 |
16 | interface IGenericThing {
17 | data: T
18 | }
19 |
20 | type IntersectionObject = { wow: string } & Bar
21 |
22 | type GenericIntersectionObject = { data: T } & Bar
23 |
24 | type AliasedString = string;
25 |
26 | type GenericScalar = GenericScalar2
27 | type GenericScalar2 = T
28 |
29 | export function bar(
30 | string: string,
31 | aliasedString: AliasedString,
32 | genericScalar: GenericScalar,
33 | array: string[],
34 | promise: Promise,
35 | anonObj: {a: number, b: string},
36 | aliasedObj: Bar,
37 | genericAliasedObj: GenericBar,
38 | interfce: IThing,
39 | genericInterface: IGenericThing,
40 | aliasedIntersectionObj: IntersectionObject,
41 | anonIntersectionObj: {num:number} & Bar,
42 | genericIntersectionObj: GenericIntersectionObject,
43 | ): string {
44 | return 'hello';
45 | }
46 |
--------------------------------------------------------------------------------
/src/test/data/void_types.ts:
--------------------------------------------------------------------------------
1 |
2 | // Infinite loop void bug: https://github.com/hasura/ndc-typescript-deno/issues/45
3 |
4 | export function void_function(): void {
5 | return
6 | }
7 |
--------------------------------------------------------------------------------
/src/test/external_dependencies_test.ts:
--------------------------------------------------------------------------------
1 |
2 | import * as test from "https://deno.land/std@0.208.0/assert/mod.ts";
3 | import * as path from "https://deno.land/std@0.208.0/path/mod.ts";
4 | import * as infer from '../infer.ts';
5 | import { FunctionNdcKind } from "../schema.ts";
6 |
7 | // Skipped due to NPM dependency resolution not currently being supported.
8 | Deno.test("External Dependencies", () => {
9 | const program_path = path.fromFileUrl(import.meta.resolve('./data/external_dependencies.ts'));
10 | const vendor_path = path.fromFileUrl(import.meta.resolve('./vendor'));
11 | const program_schema = infer.inferProgramSchema(program_path, vendor_path, true);
12 |
13 | test.assertEquals(program_schema, {
14 | scalar_types: {
15 | String: {},
16 | },
17 | object_types: {},
18 | functions: {
19 | "test_deps": {
20 | ndc_kind: FunctionNdcKind.Procedure,
21 | description: null,
22 | arguments: [
23 | {
24 | argument_name: "s",
25 | description: null,
26 | type: {
27 | name: "String",
28 | kind: "scalar",
29 | type: "named",
30 | }
31 | }
32 | ],
33 | result_type: {
34 | name: "String",
35 | kind: "scalar",
36 | type: "named",
37 | }
38 | }
39 | }
40 | });
41 | });
42 |
--------------------------------------------------------------------------------
/src/test/infer_test.ts:
--------------------------------------------------------------------------------
1 |
2 | import * as test from "https://deno.land/std@0.208.0/assert/mod.ts";
3 | import * as path from "https://deno.land/std@0.208.0/path/mod.ts";
4 | import * as infer from '../infer.ts';
5 | import { FunctionNdcKind, NullOrUndefinability } from "../schema.ts";
6 |
7 | Deno.test("Inference", () => {
8 | const program_path = path.fromFileUrl(import.meta.resolve('./data/program.ts'));
9 | const vendor_path = path.fromFileUrl(import.meta.resolve('./vendor'));
10 | const program_schema = infer.inferProgramSchema(program_path, vendor_path, false);
11 |
12 | test.assertEquals(program_schema, {
13 | scalar_types: {
14 | Float: {},
15 | String: {},
16 | },
17 | object_types: {},
18 | functions: {
19 | "hello": {
20 | ndc_kind: FunctionNdcKind.Procedure,
21 | description: null,
22 | arguments: [],
23 | result_type: {
24 | name: "String",
25 | kind: "scalar",
26 | type: "named",
27 | }
28 | },
29 | "add": {
30 | ndc_kind: FunctionNdcKind.Procedure,
31 | description: null,
32 | arguments: [
33 | {
34 | argument_name: "a",
35 | description: null,
36 | type: {
37 | name: "Float",
38 | kind: "scalar",
39 | type: "named",
40 | }
41 | },
42 | {
43 | argument_name: "b",
44 | description: null,
45 | type: {
46 | name: "Float",
47 | kind: "scalar",
48 | type: "named",
49 | }
50 | }
51 | ],
52 | result_type: {
53 | name: "Float",
54 | kind: "scalar",
55 | type: "named",
56 | }
57 | }
58 | }
59 | });
60 | });
61 |
62 | Deno.test("Complex Inference", () => {
63 | const program_path = path.fromFileUrl(import.meta.resolve('./data/complex.ts'));
64 | const vendor_path = path.fromFileUrl(import.meta.resolve('./vendor'));
65 | const program_schema = infer.inferProgramSchema(program_path, vendor_path, true);
66 |
67 | test.assertEquals(program_schema, {
68 | functions: {
69 | "complex": {
70 | ndc_kind: FunctionNdcKind.Procedure,
71 | description: null,
72 | arguments: [
73 | {
74 | argument_name: "a",
75 | description: null,
76 | type: {
77 | name: "Float",
78 | kind: "scalar",
79 | type: "named",
80 | }
81 | },
82 | {
83 | argument_name: "b",
84 | description: null,
85 | type: {
86 | name: "Float",
87 | kind: "scalar",
88 | type: "named",
89 | }
90 | },
91 | {
92 | argument_name: "c",
93 | description: null,
94 | type: {
95 | type: "nullable",
96 | null_or_undefinability: NullOrUndefinability.AcceptsUndefinedOnly,
97 | underlying_type: {
98 | name: "String",
99 | kind: "scalar",
100 | type: "named",
101 | }
102 | }
103 | }
104 | ],
105 | result_type: {
106 | name: "Result",
107 | kind: "object",
108 | type: "named",
109 | },
110 | }
111 | },
112 | object_types: {
113 | Result: {
114 | properties: [
115 | {
116 | property_name: "num",
117 | type: {
118 | name: "Float",
119 | kind: "scalar",
120 | type: "named",
121 | },
122 | },
123 | {
124 | property_name: "str",
125 | type: {
126 | name: "String",
127 | kind: "scalar",
128 | type: "named",
129 | },
130 | },
131 | {
132 | property_name: "bod",
133 | type: {
134 | name: "String",
135 | kind: "scalar",
136 | type: "named",
137 | },
138 | },
139 | ]
140 | },
141 | },
142 | scalar_types: {
143 | Float: {},
144 | String: {},
145 | }
146 | });
147 | });
148 |
--------------------------------------------------------------------------------
/src/test/inline_types_test.ts:
--------------------------------------------------------------------------------
1 |
2 | import * as test from "https://deno.land/std@0.208.0/assert/mod.ts";
3 | import * as path from "https://deno.land/std@0.208.0/path/mod.ts";
4 | import * as infer from '../infer.ts';
5 | import { FunctionNdcKind } from "../schema.ts";
6 |
7 | Deno.test("Inline Types", () => {
8 | const program_path = path.fromFileUrl(import.meta.resolve('./data/inline_types.ts'));
9 | const vendor_path = path.fromFileUrl(import.meta.resolve('./vendor'));
10 |
11 | const program_schema = infer.inferProgramSchema(program_path, vendor_path, false);
12 |
13 | test.assertEquals(program_schema,
14 | {
15 | scalar_types: {
16 | String: {},
17 | Float: {}
18 | },
19 | functions: {
20 | "bar": {
21 | ndc_kind: FunctionNdcKind.Procedure,
22 | description: null,
23 | arguments: [
24 | {
25 | argument_name: "x",
26 | description: null,
27 | type: {
28 | type: "named",
29 | kind: "object",
30 | name: "bar_arguments_x"
31 | }
32 | }
33 | ],
34 | result_type: {
35 | type: "named",
36 | kind: "scalar",
37 | name: "String"
38 | }
39 | }
40 | },
41 | object_types: {
42 | "bar_arguments_x": {
43 | properties: [
44 | {
45 | property_name: "a",
46 | type: {
47 | type: "named",
48 | kind: "scalar",
49 | name: "Float"
50 | }
51 | },
52 | {
53 | property_name: "b",
54 | type: {
55 | type: "named",
56 | kind: "scalar",
57 | name: "String"
58 | }
59 | },
60 | ]
61 | }
62 | }
63 | }
64 | );
65 |
66 | });
67 |
--------------------------------------------------------------------------------
/src/test/ndc_schema_test.ts:
--------------------------------------------------------------------------------
1 | import * as test from "https://deno.land/std@0.208.0/assert/mod.ts";
2 | import { FunctionNdcKind, NullOrUndefinability, ProgramSchema, get_ndc_schema } from "../schema.ts";
3 |
4 | Deno.test("NDC Schema Generation", () => {
5 | const program_schema: ProgramSchema = {
6 | functions: {
7 | "test_proc": {
8 | arguments: [
9 | {
10 | argument_name: "nullableParam",
11 | description: null,
12 | type: {
13 | type: "nullable",
14 | null_or_undefinability: NullOrUndefinability.AcceptsNullOnly,
15 | underlying_type: {
16 | kind: "scalar",
17 | name: "String",
18 | type: "named",
19 | },
20 | },
21 | },
22 | ],
23 | description: null,
24 | ndc_kind: FunctionNdcKind.Procedure,
25 | result_type: {
26 | type: "nullable",
27 | null_or_undefinability: NullOrUndefinability.AcceptsNullOnly,
28 | underlying_type: {
29 | kind: "scalar",
30 | name: "String",
31 | type: "named",
32 | }
33 | },
34 | },
35 | "test_func": {
36 | arguments: [
37 | {
38 | argument_name: "myObject",
39 | description: null,
40 | type: {
41 | kind: "object",
42 | name: "MyObject",
43 | type: "named",
44 | },
45 | },
46 | ],
47 | description: null,
48 | ndc_kind: FunctionNdcKind.Function,
49 | result_type: {
50 | type: "array",
51 | element_type: {
52 | kind: "scalar",
53 | name: "String",
54 | type: "named",
55 | }
56 | },
57 | },
58 | },
59 | object_types: {
60 | "MyObject": {
61 | properties: [
62 | {
63 | property_name: "string",
64 | type: {
65 | kind: "scalar",
66 | name: "String",
67 | type: "named",
68 | },
69 | },
70 | {
71 | property_name: "nullableString",
72 | type: {
73 | type: "nullable",
74 | null_or_undefinability: NullOrUndefinability.AcceptsNullOnly,
75 | underlying_type: {
76 | kind: "scalar",
77 | name: "String",
78 | type: "named",
79 | },
80 | },
81 | },
82 | ],
83 | },
84 | },
85 | scalar_types: {
86 | String: {},
87 | test_arguments_unionWithNull: {},
88 | },
89 | };
90 |
91 | const schema_response = get_ndc_schema(program_schema)
92 |
93 | test.assertEquals(schema_response, {
94 | collections: [],
95 | functions: [
96 | {
97 | name: "test_func",
98 | arguments: {
99 | "myObject": {
100 | type: {
101 | name: "MyObject",
102 | type: "named",
103 | },
104 | },
105 | },
106 | result_type: {
107 | type: "array",
108 | element_type: {
109 | name: "String",
110 | type: "named",
111 | }
112 | },
113 | },
114 | ],
115 | procedures: [
116 | {
117 | name: "test_proc",
118 | arguments: {
119 | "nullableParam": {
120 | type: {
121 | type: "nullable",
122 | underlying_type: {
123 | name: "String",
124 | type: "named",
125 | },
126 | },
127 | },
128 | },
129 | result_type: {
130 | type: "nullable",
131 | underlying_type: {
132 | name: "String",
133 | type: "named",
134 | }
135 | },
136 | }
137 | ],
138 | object_types: {
139 | "MyObject": {
140 | fields: {
141 | "string": {
142 | type: {
143 | name: "String",
144 | type: "named",
145 | },
146 | },
147 | "nullableString": {
148 | type: {
149 | type: "nullable",
150 | underlying_type: {
151 | name: "String",
152 | type: "named",
153 | },
154 | },
155 | },
156 | },
157 | },
158 | },
159 | scalar_types: {
160 | String: {
161 | aggregate_functions: {},
162 | comparison_operators: {}
163 | },
164 | test_arguments_unionWithNull: {
165 | aggregate_functions: {},
166 | comparison_operators: {}
167 | },
168 | }
169 | });
170 |
171 | });
172 |
--------------------------------------------------------------------------------
/src/test/nullable_types_test.ts:
--------------------------------------------------------------------------------
1 | import * as test from "https://deno.land/std@0.208.0/assert/mod.ts";
2 | import * as path from "https://deno.land/std@0.208.0/path/mod.ts";
3 | import * as infer from '../infer.ts';
4 | import { FunctionNdcKind, NullOrUndefinability } from "../schema.ts";
5 |
6 | Deno.test("Nullable Types", () => {
7 | const program_path = path.fromFileUrl(import.meta.resolve('./data/nullable_types.ts'));
8 | const vendor_path = path.fromFileUrl(import.meta.resolve('./vendor'));
9 | const program_schema = infer.inferProgramSchema(program_path, vendor_path, false);
10 |
11 | test.assertEquals(program_schema, {
12 | functions: {
13 | "test": {
14 | arguments: [
15 | {
16 | argument_name: "myObject",
17 | description: null,
18 | type: {
19 | kind: "object",
20 | name: "MyObject",
21 | type: "named",
22 | },
23 | },
24 | {
25 | argument_name: "nullableParam",
26 | description: null,
27 | type: {
28 | type: "nullable",
29 | null_or_undefinability: NullOrUndefinability.AcceptsNullOnly,
30 | underlying_type: {
31 | kind: "scalar",
32 | name: "String",
33 | type: "named",
34 | },
35 | },
36 | },
37 | {
38 | argument_name: "undefinedParam",
39 | description: null,
40 | type: {
41 | type: "nullable",
42 | null_or_undefinability: NullOrUndefinability.AcceptsUndefinedOnly,
43 | underlying_type: {
44 | kind: "scalar",
45 | name: "String",
46 | type: "named",
47 | },
48 | },
49 | },
50 | {
51 | argument_name: "nullOrUndefinedParam",
52 | description: null,
53 | type: {
54 | type: "nullable",
55 | null_or_undefinability: NullOrUndefinability.AcceptsEither,
56 | underlying_type: {
57 | kind: "scalar",
58 | name: "String",
59 | type: "named",
60 | },
61 | },
62 | },
63 | {
64 | argument_name: "unionWithNull",
65 | description: null,
66 | type: {
67 | kind: "scalar",
68 | name: "test_arguments_unionWithNull",
69 | type: "named",
70 | },
71 | },
72 | {
73 | argument_name: "optionalParam",
74 | description: null,
75 | type: {
76 | type: "nullable",
77 | null_or_undefinability: NullOrUndefinability.AcceptsUndefinedOnly,
78 | underlying_type: {
79 | kind: "scalar",
80 | name: "String",
81 | type: "named",
82 | },
83 | },
84 | },
85 | ],
86 | description: null,
87 | ndc_kind: FunctionNdcKind.Procedure,
88 | result_type: {
89 | type: "nullable",
90 | null_or_undefinability: NullOrUndefinability.AcceptsNullOnly,
91 | underlying_type: {
92 | kind: "scalar",
93 | name: "String",
94 | type: "named",
95 | }
96 | },
97 | },
98 | },
99 | object_types: {
100 | "MyObject": {
101 | properties: [
102 | {
103 | property_name: "string",
104 | type: {
105 | kind: "scalar",
106 | name: "String",
107 | type: "named",
108 | },
109 | },
110 | {
111 | property_name: "nullableString",
112 | type: {
113 | type: "nullable",
114 | null_or_undefinability: NullOrUndefinability.AcceptsNullOnly,
115 | underlying_type: {
116 | kind: "scalar",
117 | name: "String",
118 | type: "named",
119 | },
120 | },
121 | },
122 | {
123 | property_name: "optionalString",
124 | type: {
125 | type: "nullable",
126 | null_or_undefinability: NullOrUndefinability.AcceptsUndefinedOnly,
127 | underlying_type: {
128 | kind: "scalar",
129 | name: "String",
130 | type: "named",
131 | },
132 | },
133 | },
134 | {
135 | property_name: "undefinedString",
136 | type: {
137 | type: "nullable",
138 | null_or_undefinability: NullOrUndefinability.AcceptsUndefinedOnly,
139 | underlying_type: {
140 | kind: "scalar",
141 | name: "String",
142 | type: "named",
143 | },
144 | },
145 | },
146 | {
147 | property_name: "nullOrUndefinedString",
148 | type: {
149 | type: "nullable",
150 | null_or_undefinability: NullOrUndefinability.AcceptsEither,
151 | underlying_type: {
152 | kind: "scalar",
153 | name: "String",
154 | type: "named",
155 | },
156 | },
157 | },
158 | ],
159 | },
160 | },
161 | scalar_types: {
162 | String: {},
163 | test_arguments_unionWithNull: {},
164 | },
165 | });
166 | });
167 |
--------------------------------------------------------------------------------
/src/test/pg_dep_test.ts:
--------------------------------------------------------------------------------
1 |
2 | import * as test from "https://deno.land/std@0.208.0/assert/mod.ts";
3 | import * as path from "https://deno.land/std@0.208.0/path/mod.ts";
4 | import * as infer from '../infer.ts';
5 | import { FunctionNdcKind } from "../schema.ts";
6 |
7 | // NOTE: It would be good to have explicit timeout for this
8 | // See: https://github.com/denoland/deno/issues/11133
9 | // Test bug: https://github.com/hasura/ndc-typescript-deno/issues/45
10 | Deno.test("Postgres NPM Dependency", () => {
11 | const program_path = path.fromFileUrl(import.meta.resolve('./data/pg_dep.ts'));
12 | const vendor_path = path.fromFileUrl(import.meta.resolve('./vendor'));
13 | const program_schema = infer.inferProgramSchema(program_path, vendor_path, true);
14 |
15 | test.assertEquals(program_schema, {
16 | functions: {
17 | "insert_user": {
18 | ndc_kind: FunctionNdcKind.Procedure,
19 | description: null,
20 | arguments: [
21 | {
22 | argument_name: "user_name",
23 | description: null,
24 | type: {
25 | type: "named",
26 | kind: "scalar",
27 | name: "String"
28 | }
29 | }
30 | ],
31 | result_type: {
32 | type: "named",
33 | kind: "scalar",
34 | name: "insert_user_output"
35 | }
36 | },
37 | "insert_todos": {
38 | ndc_kind: FunctionNdcKind.Procedure,
39 | description: null,
40 | arguments: [
41 | {
42 | argument_name: "user_id",
43 | description: null,
44 | type: {
45 | type: "named",
46 | kind: "scalar",
47 | name: "String"
48 | }
49 | },
50 | {
51 | argument_name: "todo",
52 | description: null,
53 | type: {
54 | type: "named",
55 | kind: "scalar",
56 | name: "String"
57 | }
58 | },
59 | ],
60 | result_type: {
61 | type: "named",
62 | kind: "scalar",
63 | name: "insert_todos_output"
64 | }
65 | },
66 | "delete_todos": {
67 | ndc_kind: FunctionNdcKind.Procedure,
68 | description: null,
69 | arguments: [
70 | {
71 | argument_name: "todo_id",
72 | description: null,
73 | type: {
74 | type: "named",
75 | kind: "scalar",
76 | name: "String"
77 | }
78 | }
79 | ],
80 | result_type: {
81 | type: "named",
82 | kind: "scalar",
83 | name: "String"
84 | }
85 | },
86 | },
87 | scalar_types: {
88 | String: {},
89 | insert_todos_output: {},
90 | insert_user_output: {},
91 | },
92 | object_types: {},
93 | });
94 | });
95 |
--------------------------------------------------------------------------------
/src/test/prepare_arguments_test.ts:
--------------------------------------------------------------------------------
1 | import * as test from "https://deno.land/std@0.208.0/assert/mod.ts";
2 | import * as connector from '../connector.ts';
3 | import { FunctionDefinitions, FunctionNdcKind, NullOrUndefinability, ObjectTypeDefinitions } from "../schema.ts";
4 |
5 | Deno.test("argument ordering", () => {
6 | const function_name = "test_fn"
7 | const function_definitions: FunctionDefinitions = {
8 | [function_name]: {
9 | ndc_kind: FunctionNdcKind.Function,
10 | description: null,
11 | arguments: [
12 | {
13 | argument_name: "c",
14 | description: null,
15 | type: {
16 | type: "named",
17 | kind: "scalar",
18 | name: "Float"
19 | }
20 | },
21 | {
22 | argument_name: "a",
23 | description: null,
24 | type: {
25 | type: "named",
26 | kind: "scalar",
27 | name: "Float"
28 | }
29 | },
30 | {
31 | argument_name: "b",
32 | description: null,
33 | type: {
34 | type: "named",
35 | kind: "scalar",
36 | name: "Float"
37 | }
38 | },
39 | ],
40 | result_type: {
41 | type: "named",
42 | kind: "scalar",
43 | name: "String"
44 | }
45 | }
46 | }
47 | const object_types: ObjectTypeDefinitions = {}
48 | const args = {
49 | b: 1,
50 | a: 2,
51 | c: 3,
52 | }
53 |
54 | const prepared_args = connector.prepare_arguments(function_name, args, function_definitions, object_types);
55 |
56 | test.assertEquals(prepared_args, [ 3, 2, 1 ]);
57 | })
58 |
59 | Deno.test("nullable type coercion", async t => {
60 | const function_name = "test_fn"
61 | const function_definitions: FunctionDefinitions = {
62 | [function_name]: {
63 | ndc_kind: FunctionNdcKind.Function,
64 | description: null,
65 | arguments: [
66 | {
67 | argument_name: "nullOnlyArg",
68 | description: null,
69 | type: {
70 | type: "nullable",
71 | null_or_undefinability: NullOrUndefinability.AcceptsNullOnly,
72 | underlying_type: {
73 | type: "named",
74 | kind: "scalar",
75 | name: "String"
76 | }
77 | }
78 | },
79 | {
80 | argument_name: "undefinedOnlyArg",
81 | description: null,
82 | type: {
83 | type: "nullable",
84 | null_or_undefinability: NullOrUndefinability.AcceptsUndefinedOnly,
85 | underlying_type: {
86 | type: "named",
87 | kind: "scalar",
88 | name: "String"
89 | }
90 | }
91 | },
92 | {
93 | argument_name: "nullOrUndefinedArg",
94 | description: null,
95 | type: {
96 | type: "nullable",
97 | null_or_undefinability: NullOrUndefinability.AcceptsEither,
98 | underlying_type: {
99 | type: "named",
100 | kind: "scalar",
101 | name: "String"
102 | }
103 | }
104 | },
105 | {
106 | argument_name: "objectArg",
107 | description: null,
108 | type: {
109 | type: "named",
110 | kind: "object",
111 | name: "MyObject",
112 | }
113 | },
114 | {
115 | argument_name: "nullOnlyArrayArg",
116 | description: null,
117 | type: {
118 | type: "array",
119 | element_type: {
120 | type: "nullable",
121 | null_or_undefinability: NullOrUndefinability.AcceptsNullOnly,
122 | underlying_type: {
123 | type: "named",
124 | kind: "scalar",
125 | name: "String"
126 | }
127 | }
128 | }
129 | },
130 | {
131 | argument_name: "undefinedOnlyArrayArg",
132 | description: null,
133 | type: {
134 | type: "array",
135 | element_type: {
136 | type: "nullable",
137 | null_or_undefinability: NullOrUndefinability.AcceptsUndefinedOnly,
138 | underlying_type: {
139 | type: "named",
140 | kind: "scalar",
141 | name: "String"
142 | }
143 | }
144 | }
145 | },
146 | {
147 | argument_name: "nullOrUndefinedArrayArg",
148 | description: null,
149 | type: {
150 | type: "array",
151 | element_type: {
152 | type: "nullable",
153 | null_or_undefinability: NullOrUndefinability.AcceptsEither,
154 | underlying_type: {
155 | type: "named",
156 | kind: "scalar",
157 | name: "String"
158 | }
159 | }
160 | }
161 | },
162 | ],
163 | result_type: {
164 | type: "named",
165 | kind: "scalar",
166 | name: "String"
167 | }
168 | }
169 | }
170 | const object_types: ObjectTypeDefinitions = {
171 | "MyObject": {
172 | properties: [
173 | {
174 | property_name: "nullOnlyProp",
175 | type: {
176 | type: "nullable",
177 | null_or_undefinability: NullOrUndefinability.AcceptsNullOnly,
178 | underlying_type: {
179 | type: "named",
180 | kind: "scalar",
181 | name: "String"
182 | }
183 | }
184 | },
185 | {
186 | property_name: "undefinedOnlyProp",
187 | type: {
188 | type: "nullable",
189 | null_or_undefinability: NullOrUndefinability.AcceptsUndefinedOnly,
190 | underlying_type: {
191 | type: "named",
192 | kind: "scalar",
193 | name: "String"
194 | }
195 | }
196 | },
197 | {
198 | property_name: "nullOrUndefinedProp",
199 | type: {
200 | type: "nullable",
201 | null_or_undefinability: NullOrUndefinability.AcceptsEither,
202 | underlying_type: {
203 | type: "named",
204 | kind: "scalar",
205 | name: "String"
206 | }
207 | }
208 | }
209 | ]
210 | }
211 | }
212 | const test_cases = [
213 | {
214 | name: "all nulls",
215 | args: {
216 | nullOnlyArg: null,
217 | undefinedOnlyArg: null,
218 | nullOrUndefinedArg: null,
219 | objectArg: {
220 | nullOnlyProp: null,
221 | undefinedOnlyProp: null,
222 | nullOrUndefinedProp: null,
223 | },
224 | nullOnlyArrayArg: [null, null],
225 | undefinedOnlyArrayArg: [null, null],
226 | nullOrUndefinedArrayArg: [null, null],
227 | },
228 | expected: [
229 | null,
230 | undefined,
231 | null,
232 | { nullOnlyProp: null, undefinedOnlyProp: undefined, nullOrUndefinedProp: null },
233 | [null, null],
234 | [undefined, undefined],
235 | [null, null],
236 | ]
237 | },
238 | {
239 | name: "all undefineds",
240 | args: {
241 | nullOnlyArg: undefined,
242 | undefinedOnlyArg: undefined,
243 | nullOrUndefinedArg: undefined,
244 | objectArg: {
245 | nullOnlyProp: undefined,
246 | undefinedOnlyProp: undefined,
247 | nullOrUndefinedProp: undefined,
248 | },
249 | nullOnlyArrayArg: [undefined, undefined],
250 | undefinedOnlyArrayArg: [undefined, undefined],
251 | nullOrUndefinedArrayArg: [undefined, undefined],
252 | },
253 | expected: [
254 | null,
255 | undefined,
256 | undefined,
257 | { nullOnlyProp: null, undefinedOnlyProp: undefined, nullOrUndefinedProp: undefined },
258 | [null, null],
259 | [undefined, undefined],
260 | [undefined, undefined],
261 | ]
262 | },
263 | {
264 | name: "all missing",
265 | args: {
266 | objectArg: {},
267 | nullOnlyArrayArg: [],
268 | undefinedOnlyArrayArg: [],
269 | nullOrUndefinedArrayArg: [],
270 | },
271 | expected: [
272 | null,
273 | undefined,
274 | undefined,
275 | { nullOnlyProp: null, undefinedOnlyProp: undefined, nullOrUndefinedProp: undefined },
276 | [],
277 | [],
278 | [],
279 | ]
280 | },
281 | {
282 | name: "all valued",
283 | args: {
284 | nullOnlyArg: "a",
285 | undefinedOnlyArg: "b",
286 | nullOrUndefinedArg: "c",
287 | objectArg: {
288 | nullOnlyProp: "d",
289 | undefinedOnlyProp: "e",
290 | nullOrUndefinedProp: "f",
291 | },
292 | nullOnlyArrayArg: ["g", "h"],
293 | undefinedOnlyArrayArg: ["i", "j"],
294 | nullOrUndefinedArrayArg: ["k", "l"],
295 | },
296 | expected: [
297 | "a",
298 | "b",
299 | "c",
300 | { nullOnlyProp: "d", undefinedOnlyProp: "e", nullOrUndefinedProp: "f" },
301 | ["g", "h"],
302 | ["i", "j"],
303 | ["k", "l"],
304 | ]
305 | },
306 | ];
307 |
308 | await Promise.all(test_cases.map(test_case => t.step({
309 | name: test_case.name,
310 | fn: () => {
311 | const prepared_args = connector.prepare_arguments(function_name, test_case.args, function_definitions, object_types);
312 | test.assertEquals(prepared_args, test_case.expected);
313 | },
314 | sanitizeOps: false,
315 | sanitizeResources: false,
316 | sanitizeExit: false,
317 | })));
318 |
319 | })
320 |
--------------------------------------------------------------------------------
/src/test/recursive_types_test.ts:
--------------------------------------------------------------------------------
1 |
2 | import * as test from "https://deno.land/std@0.208.0/assert/mod.ts";
3 | import * as path from "https://deno.land/std@0.208.0/path/mod.ts";
4 | import * as infer from '../infer.ts';
5 | import { FunctionNdcKind } from "../schema.ts";
6 |
7 | Deno.test("Recursive Types", () => {
8 | const program_path = path.fromFileUrl(import.meta.resolve('./data/recursive.ts'));
9 | const vendor_path = path.fromFileUrl(import.meta.resolve('./vendor'));
10 |
11 | const program_schema = infer.inferProgramSchema(program_path, vendor_path, false);
12 |
13 | test.assertEquals(program_schema, {
14 | functions: {
15 | "bar": {
16 | ndc_kind: FunctionNdcKind.Procedure,
17 | description: null,
18 | arguments: [],
19 | result_type: {
20 | type: "named",
21 | kind: "object",
22 | name: "Foo"
23 | }
24 | }
25 | },
26 | object_types: {
27 | Foo: {
28 | properties: [
29 | {
30 | property_name: "a",
31 | type: {
32 | type: "named",
33 | kind: "scalar",
34 | name: "Float"
35 | }
36 | },
37 | {
38 | property_name: "b",
39 | type: {
40 | type: "array",
41 | element_type: {
42 | type: "named",
43 | kind: "object",
44 | name: "Foo"
45 | }
46 | }
47 | }
48 | ]
49 | },
50 | },
51 | scalar_types: {
52 | Float: {},
53 | },
54 | });
55 | });
56 |
--------------------------------------------------------------------------------
/src/test/type_parameters_test.ts:
--------------------------------------------------------------------------------
1 |
2 | import * as test from "https://deno.land/std@0.208.0/assert/mod.ts";
3 | import * as path from "https://deno.land/std@0.208.0/path/mod.ts";
4 | import * as infer from '../infer.ts';
5 | import { FunctionNdcKind } from "../schema.ts";
6 |
7 | Deno.test("Type Parameters", () => {
8 | const program_path = path.fromFileUrl(import.meta.resolve('./data/type_parameters.ts'));
9 | const vendor_path = path.fromFileUrl(import.meta.resolve('./vendor'));
10 |
11 | const program_schema = infer.inferProgramSchema(program_path, vendor_path, false);
12 |
13 | // TODO: Currently broken since parameters aren't normalised
14 |
15 | test.assertEquals(program_schema, {
16 | functions: {
17 | "bar": {
18 | ndc_kind: FunctionNdcKind.Procedure,
19 | description: null,
20 | arguments: [],
21 | result_type: {
22 | name: "Bar",
23 | kind: "object",
24 | type: "named",
25 | }
26 | }
27 | },
28 | object_types: {
29 | "Bar": {
30 | properties: [
31 | {
32 | property_name: "x",
33 | type: {
34 | type: "named",
35 | kind: "scalar",
36 | name: "Float"
37 | }
38 | },
39 | {
40 | property_name: "y",
41 | type: {
42 | type: "named",
43 | kind: "object",
44 | name: "Foo"
45 | }
46 | },
47 | ]
48 | },
49 | "Foo": {
50 | properties: [
51 | {
52 | property_name: "a",
53 | type: {
54 | type: "named",
55 | kind: "scalar",
56 | name: "Float"
57 | }
58 | },
59 | {
60 | property_name: "b",
61 | type: {
62 | type: "named",
63 | kind: "scalar",
64 | name: "String"
65 | }
66 | },
67 | ]
68 | },
69 | },
70 | scalar_types: {
71 | Float: {},
72 | String: {},
73 | },
74 | });
75 |
76 | });
77 |
--------------------------------------------------------------------------------
/src/test/validation_algorithm_test.ts:
--------------------------------------------------------------------------------
1 |
2 | import * as test from "https://deno.land/std@0.208.0/assert/mod.ts";
3 | import * as path from "https://deno.land/std@0.208.0/path/mod.ts";
4 | import * as infer from '../infer.ts';
5 | import { FunctionNdcKind } from "../schema.ts";
6 |
7 | Deno.test("Validation Algorithm", () => {
8 | const program_path = path.fromFileUrl(import.meta.resolve('./data/validation_algorithm_update.ts'));
9 | const vendor_path = path.fromFileUrl(import.meta.resolve('./vendor'));
10 |
11 | const program_results = infer.inferProgramSchema(program_path, vendor_path, false);
12 |
13 | test.assertEquals(program_results, {
14 | functions: {
15 | "bar": {
16 | ndc_kind: FunctionNdcKind.Procedure,
17 | description: null,
18 | arguments: [
19 | {
20 | argument_name: "string",
21 | description: null,
22 | type: {
23 | type: "named",
24 | kind: "scalar",
25 | name: "String"
26 | }
27 | },
28 | {
29 | argument_name: "aliasedString",
30 | description: null,
31 | type: {
32 | type: "named",
33 | kind: "scalar",
34 | name: "String"
35 | }
36 | },
37 | {
38 | argument_name: "genericScalar",
39 | description: null,
40 | type: {
41 | type: "named",
42 | kind: "scalar",
43 | name: "String"
44 | }
45 | },
46 | {
47 | argument_name: "array",
48 | description: null,
49 | type: {
50 | type: "array",
51 | element_type: {
52 | type: "named",
53 | kind: "scalar",
54 | name: "String"
55 | }
56 | }
57 | },
58 | {
59 | argument_name: "promise",
60 | description: null,
61 | type: {
62 | type: "named",
63 | kind: "scalar",
64 | name: "String"
65 | }
66 | },
67 | {
68 | argument_name: "anonObj",
69 | description: null,
70 | type: {
71 | type: "named",
72 | kind: "object",
73 | name: "bar_arguments_anonObj"
74 | }
75 | },
76 | {
77 | argument_name: "aliasedObj",
78 | description: null,
79 | type: {
80 | type: "named",
81 | kind: "object",
82 | name: "Bar"
83 | }
84 | },
85 | {
86 | argument_name: "genericAliasedObj",
87 | description: null,
88 | type: {
89 | type: "named",
90 | kind: "object",
91 | name: "GenericBar"
92 | }
93 | },
94 | {
95 | argument_name: "interfce",
96 | description: null,
97 | type: {
98 | type: "named",
99 | kind: "object",
100 | name: "IThing"
101 | }
102 | },
103 | {
104 | argument_name: "genericInterface",
105 | description: null,
106 | type: {
107 | type: "named",
108 | kind: "object",
109 | name: "IGenericThing"
110 | }
111 | },
112 | {
113 | argument_name: "aliasedIntersectionObj",
114 | description: null,
115 | type: {
116 | type: "named",
117 | kind: "object",
118 | name: "IntersectionObject"
119 | }
120 | },
121 | {
122 | argument_name: "anonIntersectionObj",
123 | description: null,
124 | type: {
125 | type: "named",
126 | kind: "object",
127 | name: "bar_arguments_anonIntersectionObj"
128 | }
129 | },
130 | {
131 | argument_name: "genericIntersectionObj",
132 | description: null,
133 | type: {
134 | type: "named",
135 | kind: "object",
136 | name: "GenericIntersectionObject"
137 | }
138 | },
139 | ],
140 | result_type: {
141 | name: "String",
142 | kind: "scalar",
143 | type: "named",
144 | }
145 | }
146 | },
147 | object_types: {
148 | "GenericBar": {
149 | properties: [
150 | {
151 | property_name: "data",
152 | type: {
153 | name: "String",
154 | kind: "scalar",
155 | type: "named",
156 | },
157 | },
158 | ],
159 | },
160 | "GenericIntersectionObject": {
161 | properties: [
162 | {
163 | property_name: "data",
164 | type: {
165 | name: "String",
166 | kind: "scalar",
167 | type: "named",
168 | },
169 | },
170 | {
171 | property_name: "test",
172 | type: {
173 | name: "String",
174 | kind: "scalar",
175 | type: "named",
176 | },
177 | },
178 | ],
179 | },
180 | Bar: {
181 | properties: [
182 | {
183 | property_name: "test",
184 | type: {
185 | name: "String",
186 | kind: "scalar",
187 | type: "named",
188 | },
189 | },
190 | ],
191 | },
192 | IGenericThing: {
193 | properties: [
194 | {
195 | property_name: "data",
196 | type: {
197 | name: "String",
198 | kind: "scalar",
199 | type: "named",
200 | },
201 | },
202 | ],
203 | },
204 | IThing: {
205 | properties: [
206 | {
207 | property_name: "prop",
208 | type: {
209 | name: "String",
210 | kind: "scalar",
211 | type: "named",
212 | },
213 | },
214 | ],
215 | },
216 | IntersectionObject: {
217 | properties: [
218 | {
219 | property_name: "wow",
220 | type: {
221 | name: "String",
222 | kind: "scalar",
223 | type: "named",
224 | },
225 | },
226 | {
227 | property_name: "test",
228 | type: {
229 | name: "String",
230 | kind: "scalar",
231 | type: "named",
232 | },
233 | },
234 | ],
235 | },
236 | bar_arguments_anonIntersectionObj: {
237 | properties: [
238 | {
239 | property_name: "num",
240 | type: {
241 | name: "Float",
242 | kind: "scalar",
243 | type: "named",
244 | },
245 | },
246 | {
247 | property_name: "test",
248 | type: {
249 | name: "String",
250 | kind: "scalar",
251 | type: "named",
252 | },
253 | },
254 | ],
255 | },
256 | bar_arguments_anonObj: {
257 | properties: [
258 | {
259 | property_name: "a",
260 | type: {
261 | name: "Float",
262 | kind: "scalar",
263 | type: "named",
264 | },
265 | },
266 | {
267 | property_name: "b",
268 | type: {
269 | name: "String",
270 | kind: "scalar",
271 | type: "named",
272 | },
273 | },
274 | ],
275 | },
276 | },
277 | scalar_types: {
278 | Float: {},
279 | String: {},
280 | }
281 | });
282 | });
283 |
--------------------------------------------------------------------------------
/src/test/void_test.ts:
--------------------------------------------------------------------------------
1 |
2 | import * as test from "https://deno.land/std@0.208.0/assert/mod.ts";
3 | import * as path from "https://deno.land/std@0.208.0/path/mod.ts";
4 | import * as infer from '../infer.ts';
5 |
6 | // NOTE: It would be good to have explicit timeout for this
7 | // See: https://github.com/denoland/deno/issues/11133
8 | // Test bug: https://github.com/hasura/ndc-typescript-deno/issues/45
9 | Deno.test("Void", () => {
10 | const program_path = path.fromFileUrl(import.meta.resolve('./data/void_types.ts'));
11 | const vendor_path = path.fromFileUrl(import.meta.resolve('./vendor'));
12 | test.assertThrows(() => {
13 | infer.inferProgramSchema(program_path, vendor_path, false);
14 | })
15 | });
16 |
--------------------------------------------------------------------------------
/src/util.ts:
--------------------------------------------------------------------------------
1 | export const unreachable = (x: never): never => { throw new Error(`Unreachable code reached! The types lied! 😭 Unexpected value: ${x}`) };
2 |
3 | export function isArray(x: unknown): x is unknown[] {
4 | return Array.isArray(x);
5 | }
6 |
7 | export function mapObjectValues(obj: { [k: string]: T }, fn: (value: T, propertyName: string) => U): Record {
8 | return Object.fromEntries(Object.entries(obj).map(([prop, val]) => [prop, fn(val, prop)]));
9 | }
10 |
11 | export function mapObject(obj: {[key: string]: I}, fn: ((key: string, value: I) => [string, O])): {[key: string]: O} {
12 | return Object.fromEntries(Object.entries(obj).map(([prop, val]) => fn(prop,val)));
13 | }
14 |
--------------------------------------------------------------------------------