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

Reference field for non-firebase reference field types: Data is not a reference error #502

Open
michi88 opened this issue May 9, 2023 · 3 comments

Comments

@michi88
Copy link

michi88 commented May 9, 2023

This may be a bit confusing but bear with me here.

While trying out this project I was stumped for a long time why a reference field was not working. I would always get Data is not a reference.

What I actually didn't know, and is not specified in the docs anywhere, is that this requires a Firebase reference field type. I'm guessing this field type is not being used a lot as it is not pushed in any Google docs as well. At least I'm not using it. The default in projects I see is to just use an ID of the referenced objects and build the paths in code to fetch data etc.

I understand that we need to provide some more data for FireCMS to be able to correctly fetch the referenced object(s) etc. but from a user perspective one would only need to provide a function to build the path maybe? Or if the path is just a collection and an entity ID, we'd only need that info.

Did a quick POC how it can work for reading/displaying these types of fields:

You could make an helper like this:

/**
 * Apparently a 'reference' field requires an actual firestore reference field
 * to work. So we need to create a field that will fetch the referenced object and
 * then render a reference preview. Then is can be added as an additional field.
 *
 * TODO: Ask FireCMS team why a reference field is actually necessary for this.
 *  it seems like a regular use-case to me and it would be great if updating
 *  a field like this would also work.
 */
function referenceFromIdFieldBuilder<
  EntityT extends { [K in ReferenceFieldId]: string },
  ReferenceFieldId extends Extract<keyof EntityT, string>,
  ReferencedCollectionT
>({
  referenceId,
  name,
  width,
  collection,
  referencePreviewProps
}: {
  referenceId: ReferenceFieldId;
  name: string;
  width?: number;
  collection: EntityCollection<ReferencedCollectionT>;
  referencePreviewProps: Omit<ReferencePreviewProps, "reference">;
}): AdditionalFieldDelegate<EntityT> {
  return {
    id: referenceId,
    name: name,
    width,
    Builder: ({ entity, context }) => {
      const obj = entity.values;
      return (
        <AsyncPreviewComponent
          builder={context.dataSource
            .fetchEntity({
              path: collection.path,
              entityId: obj[referenceId],
              collection: collection
            })
            .then(refEntity => {
              if (!refEntity) {
                return undefined;
              }
              const reference = new EntityReference(
                refEntity.id,
                refEntity.path
              );
              return (
                <ReferencePreview
                  disabled={referencePreviewProps.disabled}
                  size={referencePreviewProps.size}
                  reference={reference}
                  previewProperties={referencePreviewProps.previewProperties}
                ></ReferencePreview>
              );
            })}
        />
      );
    },
    dependencies: [referenceId]
  };
}

Suppose you have an Organization{name: string} collection and an Account{name: string, organizationId: string} collection.
You can then use it in additional fields like so:

const orgsCollection = buildCollection<Organization>({
  name: "Organizations",
  singularName: "Organization",
  path: dbPaths.organizations,
  properties: {
    name: {
      name: "Name",
      readOnly: true,
      dataType: "string"
    }
  }
});

const accountCollection = buildCollection<Account>({
  name: "Accounts",
  singularName: "Account",
  path: dbPaths.accounts,
  properties: {
    name: {
      name: "Name",
      readOnly: true,
      dataType: "string"
    },
  },
  additionalFields: [
    referenceFromIdFieldBuilder<Account, "organizationId", Organization>({
      referenceId: "organizationId",
      name: "Organization",
      collection: orgsCollection,
      referencePreviewProps: {
        disabled: false,
        size: "regular",
        previewProperties: ["name"]
      }
    })
  ]
});

This way at least we can render these fields but updating of course does not work.

Questions:

  1. Am I missing something here / is there a better way?
  2. Could we add a data type that supports updating if we can have the user configure the path in some way?
  3. Can we update the docs to make it clear that dataType: 'reference' requires a firebase reference?

For number 2. Say we have this fictional referenceById datatype:

const accountCollection = buildCollection<Account>({
  name: "Accounts",
  singularName: "Account",
  path: dbPaths.accounts,
  properties: {
    name: {
      name: "Name",
      readOnly: true,
      dataType: "string"
    },
    organizationId: {
      dataType: "referenceById",
      path: "organizations",
      name: "Related organization"
    }
  }
});

Then we'd know the base path ("organizations"), the field that holds the ID ("organizationId") and the value we have while rendering the entity. I understand this does not always work when dealing with subcollections and stuff so maybe a pathBuilder func needs to be provided by the user that takes the entity being rendered / updated or something but initially this was why I was confused as I thought this should work.

Hoping this issue will also save some googlers (and future ChatGPT ;-)) time figuring out what is going on.

Last but not least, thanks for the efforts here, I think FireCMS is something the Firebase ecosystem is lacking. We haven't decided yet if we'll adopt this in our projects but if we will, we will be sponsoring. Keep up the good work!

@fgatti675
Copy link
Member

Hi @michi88
Before jumping into your code to investigate, if I understand correctly, you are looking for a solution for storing a reference path like a string, but still be able to use the reference functionality of the CMS.
If that is the case, the simplest way I can think of making it work is using EntityCallbacks: onFetch and onPreSave for adapting your data model.
Also, keep in mind that you don't need to fetch the entity for building the reference as you do inside AsyncPreviewComponent, the path and id retrieved will be the same ones you input.
Hope this helps

@michi88
Copy link
Author

michi88 commented Aug 7, 2023

Thanks for leaving a comment here @fgatti675
What do you mean by that I do not need to fetch the entity? I'm inputting entityId: obj[referenceId] which would be the id stored in the main document together with the path from the config, right?

@michi88
Copy link
Author

michi88 commented Aug 7, 2023

Also, when I'm using EntityCallbacks: onFetch and set the fetched values on the object as a map, they are only rendered in the list when I open the details dialog. Any way to get around that? I guess AsyncPreviewComponent should be used somehow? As I want to show multiple fields of a doc that needs to be fetched, I'd like to avoid loading the related doc for every field.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants