aws-amplify / amplify-js

A declarative JavaScript library for application development using cloud services.
https://docs.amplify.aws/lib/q/platform/js
Apache License 2.0
9.41k stars 2.12k forks source link

Serializable instances of DataStore models #11170

Open charlieforward9 opened 1 year ago

charlieforward9 commented 1 year ago

Is this related to a new or existing framework?

React

Is this related to a new or existing API?

DataStore

Is this related to another service?

No response

Describe the feature you'd like to request

I would like to be able to store DataStore instances in my Redux state variables. For example:

With this Account data model:

type EagerAccount = {
  readonly [__modelMeta__]: {
    identifier: ManagedIdentifier<Account, 'id'>;
    readOnlyFields: 'createdAt' | 'updatedAt';
  };
  readonly id: string;
  readonly type: number;
  readonly creationDate: string;
  readonly billing?: Billing | null;
  readonly Users?: User[] | null;
  readonly Groves?: (Grove | null)[] | null;
  readonly createdAt?: string | null;
  readonly updatedAt?: string | null;
}

type LazyAccount = {
  readonly [__modelMeta__]: {
    identifier: ManagedIdentifier<Account, 'id'>;
    readOnlyFields: 'createdAt' | 'updatedAt';
  };
  readonly id: string;
  readonly type: number;
  readonly creationDate: string;
  readonly billing?: Billing | null;
  readonly Users: AsyncCollection<User>;
  readonly Groves: AsyncCollection<Grove>;
  readonly createdAt?: string | null;
  readonly updatedAt?: string | null;
}

export declare type Account = LazyLoading extends LazyLoadingDisabled ? EagerAccount : LazyAccount

export declare const Account: (new (init: ModelInit<Account>) => Account) & {
  copyOf(source: Account, mutator: (draft: MutableModel<Account>) => MutableModel<Account> | void): Account;
}

I have this snippet

interface AuthState {
  ...
  account: Account | null;
}

const initialState: AuthState = {
  ...
  account: null,
};
const authSlice = createSlice({
    ....
      const account = new Account({
        type: action.payload.accountType,
        creationDate: new Date().toISOString().slice(0, 10),
      });
      state.account = account;
});

That returns this error A non-serializable value was detected in the state, in the path: auth.account

Describe the solution you'd like

I would like to be able to call something like account.serialize(), which would return a serialized version of Account.

Describe alternatives you've considered

I have created Account instance and use the helper function to convert it into a serializable object, but this adds more complexity to my codebase, which will need to be manually changed whenever the data model changes through amplify pull. It seems more suitable to have this functionality within the generated models/ folder.

Additional context

No response

Is this something that you'd be interested in working on?

chrisbonifacio commented 1 year ago

Hi @charlieforward9 👋 thanks for raising this issue.

We have some utility functions for serializing and deserializing datastore models.

Example:

import { serializeModel } from '@aws-amplify/datastore/ssr';

//...
interface AuthState {
  //...
  account: Account | null;
}

const initialState: AuthState = {
  //...
  account: null,
};

const authSlice = createSlice({
    //....
      const account = new Account({
        type: action.payload.accountType,
        creationDate: new Date().toISOString().slice(0, 10),
      });
      state.account = serializeModel(account);
});
}

These were intended for serializing models so that they may be passed from server to client and vice versa so perhaps this fits your use case?

charlieforward9 commented 1 year ago

Thank you for the quick response. That seems like it should fit my needs as it is converted to JSON!

charlieforward9 commented 1 year ago

Hello Amplify team,

As my repository grows in complexity, I would like to know if there is a solution for this growing issue:

I need serializable models for state management purposes. However, if the data model changes, I have to propagate these changes manually over to the serializable model instances, this can lead to bugs that can take some time to catch.

Schema Snippet ```graphql type Account @model @auth(rules: [{ allow: public }]) { id: ID! type: Int! creationDate: AWSDate! billing: Billing Users: [User!] @hasMany(indexName: "byAccount", fields: ["id"]) Groves: [Grove] @hasMany(indexName: "byAccount", fields: ["id"]) } type User @model @auth(rules: [{ allow: public }]) { id: ID! email: AWSEmail! type: Int! firstName: String lastName: String phone: AWSPhone accountID: ID! @index(name: "byAccount") Account: Account! @belongsTo(fields: ["accountID"]) Uploads: [Upload] @hasMany(indexName: "byUser", fields: ["id"]) Orders: [Order] @hasMany(indexName: "byUser", fields: ["id"]) } ```
Amplify-generated model types (Non-serializable) ```typescript type EagerAccount = { readonly [__modelMeta__]: { identifier: ManagedIdentifier; readOnlyFields: 'createdAt' | 'updatedAt'; }; readonly id: string; readonly type: number; readonly creationDate: string; readonly billing?: Billing | null; readonly Users?: User[] | null; readonly Groves?: (Grove | null)[] | null; readonly createdAt?: string | null; readonly updatedAt?: string | null; } type LazyAccount = { readonly [__modelMeta__]: { identifier: ManagedIdentifier; readOnlyFields: 'createdAt' | 'updatedAt'; }; readonly id: string; readonly type: number; readonly creationDate: string; readonly billing?: Billing | null; readonly Users: AsyncCollection; readonly Groves: AsyncCollection; readonly createdAt?: string | null; readonly updatedAt?: string | null; } export declare type Account = LazyLoading extends LazyLoadingDisabled ? EagerAccount : LazyAccount export declare const Account: (new (init: ModelInit) => Account) & { copyOf(source: Account, mutator: (draft: MutableModel) => MutableModel | void): Account; } type EagerUser = { readonly [__modelMeta__]: { identifier: ManagedIdentifier; readOnlyFields: 'createdAt' | 'updatedAt'; }; readonly id: string; readonly email: string; readonly type: number; readonly firstName?: string | null; readonly lastName?: string | null; readonly phone?: string | null; readonly accountID: string; readonly Account: Account; readonly Uploads?: (Upload | null)[] | null; readonly Orders?: (Order | null)[] | null; readonly createdAt?: string | null; readonly updatedAt?: string | null; } type LazyUser = { readonly [__modelMeta__]: { identifier: ManagedIdentifier; readOnlyFields: 'createdAt' | 'updatedAt'; }; readonly id: string; readonly email: string; readonly type: number; readonly firstName?: string | null; readonly lastName?: string | null; readonly phone?: string | null; readonly accountID: string; readonly Account: AsyncItem; readonly Uploads: AsyncCollection; readonly Orders: AsyncCollection; readonly createdAt?: string | null; readonly updatedAt?: string | null; } export declare type User = LazyLoading extends LazyLoadingDisabled ? EagerUser : LazyUser export declare const User: (new (init: ModelInit) => User) & { copyOf(source: User, mutator: (draft: MutableModel) => MutableModel | void): User; } ```
Manually-written interfaces (serializable counterparts) ```typescript interface serializedAccount { id: string; type: number; creationDate: string; billing?: Billing | null; Users?: serializedUser[] | null; Groves?: (serializedGrove | null)[] | null; createdAt?: string | null; updatedAt?: string | null; } interface serializedUser { id: string; email: string; type: number; firstName?: string | null; lastName?: string | null; phone?: string | null; accountID: string; Account: serializedAccount; Uploads?: (serializedUpload | null)[] | null; Orders?: (serializedOrder | null)[] | null; createdAt?: string | null; updatedAt?: string | null; } ```

The best solution in mind is to have the serializable versions generated in the models/index.d.ts file, where they can be used without any extra manually written code, and update to data model changes automatically.

However, I may be wrong. What does the Amplify team think? Is there already a solution out there for this use case that extends the functionality of serializeModel() into a easy-to-use, easy-to-update solution?

chrisbonifacio commented 10 months ago

Hi @charlieforward9 I will mark this as a feature request for now and discuss it with the team

charlieforward9 commented 9 months ago

@chrisbonifacio I am upgrading to v6 and the serializeModel utility is no longer available in the @aws-amplify/datastore/ssr import path. Has it been moved? Has it been deprecated?

Edit: I found the function in the v5 source code, which was honestly simpler than I expected.

// Helper for converting DataStore models to JSON
export function serializeModel<T extends PersistentModel>(
    model: T | T[]
): JSON {
    return JSON.parse(JSON.stringify(model));
}

I will just add this to my codebase until this feature request is considered.

chrisbonifacio commented 9 months ago

Hi @charlieforward9 👋

Unfortunately, with Amplify v6, DataStore is not supported in an SSR context. We recommend using the Amplify API library on the server side.

charlieforward9 commented 6 months ago

For anyone who lands on here looking for a solution, I ended up removing DataStore from my project and interact exclusively with the GraphQL API to get serializable objects from the get go.