Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(nango.yaml): dedicated package, stricter parsing, wider types support #2303

Merged
merged 54 commits into from
Jun 18, 2024

Conversation

bodinsamuel
Copy link
Collaborator

@bodinsamuel bodinsamuel commented Jun 11, 2024

Fixes NAN-1166
Fixes https://linear.app/nango/issue/NAN-1096/add-comment-at-the-top-of-modelsts-file-to-explain-that-its-auto

🤓 Context

First I want to apologize for the length of this PR, it's way too big but it was "all or nothing" situation unfortunately.

When I started playing with types/json schema validation I wanted better output for errors at compile time:

  • realized the CLI had 2 different ways to parse the nango.yaml
  • realized nango-config.service.ts was not covering the same case that the one in cli
  • realized neither were not handling many types/edge cases (i.e: arrays, number, boolean, union, exotic char in model name, etc.)
  • then after modifying everything (took me a day), I realized it was also parsing the table sync_configs (for a reason I still don't get) so it was just breaking everything.

So I had no choice, to rewrite everything just because I wanted to cover more cases to be compatible with json schema 😵‍💫

The Good news:

  • we now have a dedicated package to parse nango-yaml and it's battle tested (hopefully)
  • we now have one less thing to remove from shared (nb: the old code is still there and it's still used by sync_configs)
    There is only 13 mentions to shared left in CLI

🚜 Changes

  • New published package @nangohq/nango-yaml
    It will be published, as soon as you validate this PR otherwise the CLI won't pass.

  • New output when validating nango.yaml
    A lot of new edge cases, errors and warnings are now discovered. It might be breaking but at least it's explicit.

  • [breaking] endpoint format is enforced (verb /uri)

  • [breaking] runs is explicitly required

  • [breaking] format of NangoFlows inside model.ts

  • [breaking] Input and Output specifying a js value (instead of a model) now use a type interface with a unique generated name

  • [new] Support for unions, arrays, type array, cyclic ref, literal string, number, true, false, null, undefined, multiple extends

  • [fix] Disallow exotic char in model and property name

  • [fix] Always compile model.ts after loading nango.yaml

  • ...not exhaustive changelist

Warning

UI won't show the appropriate model (fixing that later) and this is a breaking change for GET /flows GET /config/:key

😭 How do I review this huge PR?

  • Starts with: packages/nango-yaml/parser.v2.ts
  • You can read: packages/nango-yaml/modelsParser.ts which handle the yaml -> js types
  • Then packages/cli/services/model.service.ts
  • Optional packages/types/nangoYaml/index.ts (describe yaml then parsed yaml)
  • And voila, the rest is mostly side effects of having good types and slightly different objects

🧪 How do I test?

Please run the cli on your machine, I have extensively tested but it's so critical and super user dependent.

node ../nango/packages/cli/dist/index.js init 
node ../nango/packages/cli/dist/index.js generate // try to delete a script and make it re-generate it 
node ../nango/packages/cli/dist/index.js compile // check your model.ts
node ../nango/packages/cli/dist/index.js deploy dev
models:
  Test1:
    id: number
    str: "literal"
    issue: Unknown
    arr:
      - hello
      - boy

  Test2:
      id: integer
      title: string
      url: string
  Parent:
      id: integer
  Test3:
      __extends: Parent
      oui: true
      non: false
      OUI: True
      NON: False
      num: 1
      kd?: Pick<dfdk>
      nulle: null
      union: True | False
      nes:
        ted: number
   Test4:
     __string: number

Example

Screenshot 2024-06-13 at 11 40 10

@bodinsamuel bodinsamuel self-assigned this Jun 11, 2024
Copy link

linear bot commented Jun 13, 2024

return true;
}

parseFields({ fields, parent }: { fields: NangoYamlModelFields; parent: string }): NangoModelField[] {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the main function to parse models and handle all the edge cases regarding types

@@ -0,0 +1,154 @@
import { expect, describe, it } from 'vitest';
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved from shared

return !regQuote.test(name);
}

export function getProviderConfigurationFromPath({ filePath, parsed }: { filePath: string; parsed: NangoYamlParsed }): NangoYamlParsedIntegration | null {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved from shared

.map((v) => v.toLocaleLowerCase());
const typesWithGenerics = [...JAVASCRIPT_AND_TYPESCRIPT_TYPES.builtInObjects, ...JAVASCRIPT_AND_TYPESCRIPT_TYPES.utilityTypes];
// Only used externally
export function isJsOrTsType(type?: string): boolean {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved it before realizing it wasn't what I expected, it will be deleted as soon as I migrate the backend to use the new schema

* and then 1636 etc. The offset should be based on the interval and should never be
* greater than the interval
*/
export function getInterval(runs: string, date: Date): IntervalResponse | Error {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved from shared, with the diff that ServiceResponse does not exists and not available in CLI so I just return an Error if any

offset: number;
}

export function determineVersion(configData: NangoYaml): 'v1' | 'v2' {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved from shared

"copy:files": "copyfiles -u 1 lib/templates/* dist",
"copy:types": "cp `node -p \"require.resolve('@nangohq/shared/dist/sdk/sync.d.ts')\"` './dist/nango-sync.d.ts'",
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this one is a big bet, I'm trying to load the file from user's global node_modules, should work but let's see :D

expect(modelsFile).not.toContain(`'Other[]' | null | undefined;`);
expect(modelsFile).toContain(`Other[] | null | undefined;`);
});

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of this has been moved either to model.service.ts or removed because no longer relevant

@khaliqgant
Copy link
Member

khaliqgant commented Jun 17, 2024

Could not reproduce, happy to dig if you send me the customer name in DM

This seems to happen when there is a type used in the sync instead of a sync_type

syncs:
          salesforce-contacts:
              type: full
              runs: every day
              auto_start: false
              output: Contact
              input: FieldSchema
              endpoint: /salesforce/contact
              scopes: offline_access,api
              description: |
                Syncs contacts based on a metedata defined object. Requires metadata
                Auto start false.
              webhook-subscriptions:
                - contact.update

with this configuration gives:

➜  nango-integrations git:(main) ✗ npx nango scc
nango.yaml validation failed

integrations > salesforce > syncs > salesforce-contacts
  error must NOT have additional properties [additionalProperties]
  additionalProperty: type

[object Object]
null

If a model is reference but doesn't actually exist

It seems the __string isn't handled correctly:

  Contact:
    id: string
    userId: string
    __string: string

produces:

export interface Contact {
  [key: string]: 'string';
  id: string;
  userId: string;
};

where previously it would correctly output:

export interface Contact {
  id: string;
  userId: string;
 [key: string]: string;
}

Given this:

        actions:
          salesforce-fetch-nested-fields:
              description: Fetch available fields for a nested field
              input: string
              endpoint: /salesforce/fetch-nested-fields
              output: NestedFieldSchema
              scopes: offline_access,api

The outputted model is

"input": "Anonymous_salesforce_action_salesforcefetchnestedfields_input"

which is unexpected. I see why you did it to encourage people to use models but IMO an input of a string is perfectly valid and shouldn't require a model. But maybe this is more of a product question and good for Bastien to weigh in.

Also if a model is declared but doesn't actually exist the yaml parsing continues but feel like we should catch this?

@khaliqgant
Copy link
Member

When running nango scc I definitely miss getting the fields outputted -- not sure if this is coming in the piepline of your work. It used to output this:

{
        "name": "salesforce-fetch-fields",
        "type": "action",
        "models": [
          {
            "name": "FieldSchema",
            "fields": [
              {
                "name": "fields",
                "type": "Field[]"
              },
              {
                "name": "childRelationships",
                "type": "ChildField[]"
              },
              {
                "name": "userIdField",
                "type": "string | undefined | null"
              }
            ]
          },

now just gives

        {
          "name": "salesforce-fetch-fields",
          "type": "action",
          "description": "Fetch available contact fields",
          "scopes": [
            "offline_access",
            "api"
          ],
          "input": null,
          "output": [
            "FieldSchema"
          ],
          "usedModels": [
            "FieldSchema"
          ],
          "endpoint": {
            "POST": "/salesforce/fetch-fields"
          }
        }
      ]

@khaliqgant
Copy link
Member

[new] Support for unions, arrays, type array, cyclic ref, literal string, number, true, false, null, undefined, multiple extends

Can you give examples of each of these?

multiple extends works nicely!

  SelectedField:
    name: string
    label: string
    type: string
    foo:
      __extends: Entity

@bodinsamuel
Copy link
Collaborator Author

This seems to happen when there is a type used in the sync instead of a sync_type

type was never officially supported (cf the json schema) so to me I just fixed the validation.

It seems the __string isn't handled correctly:

✅ Thanks, missed that in my tests

[object Object]

✅ Okay found the issue thanks

I definitely miss getting the fields outputted

Added the models output, but yeah I'm not keeping the raw format in memory

input of a string is perfectly valid and shouldn't require a model

It's still perfectly valid, but I need one model per input/output so that I can generate json schema (and more) in a single way. With just one way there is no need to check everywhere in the backend if a string is jsType or a model.

Also if a model is declared but doesn't actually exist the yaml parsing continues but feel like we should catch this?

That's the problem of supporting js types, there is no way to differentiate literal string and model name. So I added a warning about literal string but I don't see a better way rn

[new] Support for unions, arrays, type array, cyclic ref, literal string, number, true, false, null, undefined, multiple extends

You have in the test and PR desc.

models:
  Ref:
    id: string
  Ref2:
    name: string
  Base:
    unions: string | null | Ref | true[]
    arrUnion:
      - Ref
      - true
    literal: hello
    num: 1
    literalNull: null[]
    obj:
      nested: string
    optional?: string
    __extends: Ref,Ref2

@bodinsamuel bodinsamuel requested a review from TBonnin June 17, 2024 12:42
import { getNativeDataType, getPotentialTypeAlias, isDisallowedType, shouldQuote } from './helpers.js';
import { ParserError, ParserErrorCycle, ParserErrorDataSyntax, ParserErrorExtendsNotFound, ParserErrorInvalidModelName } from './errors.js';

export class ModelsParser {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small nit but I would keep this singular, ModelParser

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see what you mean, it's like that because it's parsing the models list and contains all the models maybe ModelsPropParser would have been clearer

Copy link
Member

@khaliqgant khaliqgant left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small nits, but works well. I was using this branch as I was writing customer scripts today 💯

This is a bit weird:

        {
          "name": "selectedFields",
          "value": [
            {
              "name": "0",
              "value": "SelectedField[]",
              "model": true,
              "optional": false
            },
            {
              "name": "1",
              "tsType": true,
              "array": false,
              "optional": false
            },
            {
              "name": "2",
              "value": null,
              "tsType": true,
              "array": false,
              "optional": false
            }
          ],
          "union": true,
          "optional": false
        }

This is when the type is like this

selectedFields: SelectedField[] | undefined | null

weird in the sense that the name is the array index, but get that this might be technically challenging.

@bodinsamuel
Copy link
Collaborator Author

SelectedField[] | undefined | null

Interesting, actually it should be supported. Adding this edge case, thanks a lot for the thorough testing 🙏🏻

@bodinsamuel bodinsamuel enabled auto-merge (squash) June 18, 2024 07:16
@bodinsamuel bodinsamuel merged commit 4012b2d into master Jun 18, 2024
20 checks passed
@bodinsamuel bodinsamuel deleted the fix/cli-harden-validation branch June 18, 2024 07:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants