#Type help and mapping dynamic collections

16 messages · Page 1 of 1 (latest)

barren cave
#

I'll explain more what I'm actually trying to do:

The server is expecting this type from the client:

export type FormInputV1 = {
  congestion?: InputMaybe<CongestionInputV1>;
  disciplineAndMaxDepth?: InputMaybe<DisciplineAndMaxDepthInputV1>;
  maxDepth?: InputMaybe<MaxDepthInputV1>;
  reportName?: InputMaybe<ReportNameInputV1>;
  visibility?: InputMaybe<VisibilityInputV1>;
  weather?: InputMaybe<WeatherInputV1>;
  wildlife?: InputMaybe<WildlifeInputV1>;
};

Regarding UX: the goal is that rather than having to sift through all the optional fields every time the user goes to log their dive (because there will be many more optional fields), they are able to choose which fields they want to appear in their own form. And then they reuse that form.

So, the client returns a concrete version of this type, with the fields they want.

That's the form builder.

Now they have a concrete type in the database, that they use to build reports on, i.e. fill out the form they created.

The form builder looks like this right now:


export function FormBuilder() {
  let navigation = useNavigation<AllNavigationProps>();

  const { putFormMutation, result } = useInsertForm();

  type ImportValues = Record<
    keyof FormInputV1,
    { active: boolean; fieldOrder: number }
  >;
  let allFields = FormV1Wrapper.getKeyArray();
  type LocalValues = { name: string };
  type FormValues = ImportValues & LocalValues;

  const {
    control,
    handleSubmit,
    formState: { errors },
  } = useForm<FormValues>({});

  const onSubmit: SubmitHandler<FormValues> = (formData) => {
    let newForm = FormV1Wrapper.createForm(formData);

    const formInput: FormInput = {
      v1: newForm,
    };

    // TODO: Add originalform and previousform for "editing"?
    const formDetailsInput: FormDetailsInput = {
      formName: "memes",
      // originalFormId: _,
      // previousFormId: _
    };

    putFormMutation({ variables: { formDetailsInput, formInput } });
  };
  ...
}

#

What I was doing here was mapping over all the keys in FormInputV1 and adding a new value to it: an object with an active and fieldOrder key/value pairs. Since all the user cares about in this step is which fields they want in their form, and in what order they want them to appear.

#

I am handling the form creation in this Class:

export class FormV1Wrapper {
  readonly formOutput?: FormOutputV1;
  constructor(formOutput: FormOutputV1) {
    this.formOutput = formOutput;
  }

  static getKeyArray() {
    type FormValues = Record<keyof FormInputV1, boolean>;
    const allFields: (keyof FormValues)[] = [
      "congestion",
      "disciplineAndMaxDepth",
      "maxDepth",
      "reportName",
      "visibility",
      "weather",
      "wildlife",
    ];
    return allFields;
  }

  static createForm(
    f: Record<keyof FormInputV1, { active: boolean; fieldOrder: number }>
  ): FormInputV1 {
    let newForm: FormInputV1 = {
      congestion: f.congestion.active
        ? { fieldOrder: f.congestion.fieldOrder }
        : undefined,
      disciplineAndMaxDepth: f.disciplineAndMaxDepth.active
        ? { fieldOrder: f.disciplineAndMaxDepth.fieldOrder }
        : undefined,
      maxDepth: f.maxDepth.active
        ? { fieldOrder: f.maxDepth.fieldOrder }
        : undefined,
      reportName: f.reportName.active
        ? { fieldOrder: f.reportName.fieldOrder }
        : undefined,
      visibility: f.visibility.active
        ? { fieldOrder: f.visibility.fieldOrder }
        : undefined,
      weather: f.weather.active
        ? { fieldOrder: f.weather.fieldOrder }
        : undefined,
      wildlife: f.wildlife.active
        ? { fieldOrder: f.wildlife.fieldOrder }
        : undefined,
    };

    return newForm;
  }
}
#

Now, this is where I feel like I could be leveraging typescript better.

#

btw, I'm open to changing how the backend works too...

carmine blade
#

ah, i see the values are heterogeneous and correlated with the particular key, so my suggestion to use a Map is probably not a good one in this case

#

i haven't fully digested this and have some real work to do so i can't give specific advice, but if i have time later i'll take a closer look

barren cave
#

@carmine blade I'd appreciate that, thanks!

barren cave
#

Here I have the FormsList, which is a list of all the forms the user has created:

export function FormsList() {
  let navigation = useNavigation<AllNavigationProps>();
  const { loading, error, data } = useGetForms();
  if (error) {
    console.error(error);
  }

  const handleFormPress = (form: Form) => {
    navigation.navigate("FormFiller", { form });
  };

  return (
    <>
      {loading && (
        <View>
          <CoreText>Loading Forms...</CoreText>
        </View>
      )}
      {data?.forms.map((f, i) => {
        return (
          <Pressable onPress={() => handleFormPress(f)} key={f.id + i}>
            <CoreText>{f.formName}</CoreText>
            <CoreText>Created: {f.createdAt}</CoreText>
            <View>
              {f.formData.congestion && (
                <View>
                  <CoreText>Congestion</CoreText>
                </View>
              )}
              {f.formData.disciplineAndMaxDepth && (
                <View>
                  <CoreText>disciplineAndMaxDepth</CoreText>
                </View>
              )}
              {f.formData.maxDepth && (
                <View>
                  <CoreText>maxDepth</CoreText>
                </View>
              )}
              {f.formData.reportName && (
                <View>
                  <CoreText>reportName</CoreText>
                </View>
              )}
              {f.formData.visibility && (
                <View>
                  <CoreText>visibility</CoreText>
                </View>
              )}
              {f.formData.weather && (
                <View>
                  <CoreText>weather</CoreText>
                </View>
              )}
              {f.formData.wildlife && (
                <View>
                  <CoreText>wildlife</CoreText>
                </View>
              )}
            </View>
          </Pressable>
        );
      })}
    </>
  );
}
#

It feels like a lot of boiler plate to write everything out right now. Also, I haven't taken into account ordering. The fields are ordered, but the order is in each object...

carmine blade
#

i threw what you shared into a playground so i can poke at it, but i had to make some guesses. before i go further, let me know if this is at least correct enough to get the main gist of things:

#

oof, playground link too long. one sec

barren cave
#

It's very cool to see your experiments

#

@carmine blade The types are more or less correct. Why I don't I just save you some time and point you at a couple places in the repo? I feel like I've been working in a vacuum with this and I'd love to hear your thoughts.

It has evolved a bunch since we last spoke, so here is my current version of the formV1Wrapper.ts
https://github.com/AJTJ/free-app/blob/main/utility/formV1Wrapper.ts

You can see the generated types (through graphql-codegen) here:
https://github.com/AJTJ/free-app/blob/main/api/types/types.generated.ts

I'm not sure of your knowledge of databases but one thing I've been reconsidering is how I store the forms and reports that the user creates.

For clarification: "forms" are forms that the user creates from a selection of fields.
And "reports" are the reports that the user creates from those forms.

This is how I'm currently doing it.

I've created FormV1 which is a key-value type of optional fields, eventually it will have a LOT more optional fields.

export type FormV1Request = {
  congestion?: InputMaybe<CongestionV1Request>;
  disciplineAndMaxDepth?: InputMaybe<DisciplineAndMaxDepthV1Request>;
  maxDepth?: InputMaybe<MaxDepthV1Request>;
  sessionName?: InputMaybe<SessionNameV1Request>;
  visibility?: InputMaybe<VisibilityV1Request>;
  weather?: InputMaybe<WeatherV1Request>;
  wildlife?: InputMaybe<WildlifeV1Request>;
};

export type CongestionV1Request = {
  fieldOrder?: InputMaybe<Scalars['Int']>;
  value?: InputMaybe<Scalars['Int']>;
};

export type MaxDepthV1Request = {
  fieldOrder?: InputMaybe<Scalars['Int']>;
  maxDepth?: InputMaybe<Scalars['Int']>;
};

The user can create their form from it, like so:

const myFirstForm: FormV1Request = {
  congestion: { fieldOrder: 0 },
  maxDepth: { fieldOrder: 1 }
}

And then they create a report with it like so:

const myFirstReport: FormV1Request = {
  congestion: { fieldOrder: 0, value: 100 },
  maxDepth: { fieldOrder: 1, maxDepth: 50 }
}
#

Currently it's both the user's forms AND their reports are being stored as separate jsonb objects in one column of either the forms or reports table in my postgres database.

Now, I'm going to want to use the reports data in order to put pins on a map (for location) and then also perform data analysis. I'm wondering if the data format I've chosen is going to be bad.

Furthermore, I was considering storing an array of fields in the database, to preserve order more simply. But I feel that might make it even slower to perform the data analysis I'm proposing.