Resource

Resources are the fundamental units of Atomic Data. All data fetched using the store is represented as a resource. In @tomic/lib resources are instances of the Resource class.

Getting a resource

A resource can be obtained by either fetching it from the server or by creating a new one. To fetch a resource, use the store.getResource method.

const resource = await store.getResource('https://my-resource-subject.com');

Read more about fetching resources.

Creating a new resource is done using the store.newResource method.

const resource = await store.newResource();

Read more about creating resources.

Typescript

Resources can be annotated with the subject of a class. This subject has to be known to @tomic/lib.

import { type Article } from './ontologies/article'; // File generated by @tomic/cli

const resource = await store.getResource<Article>(
  'https://my-resource-subject.com',
);

Annotating resources opens up a lot of great dev experience improvements, such as autocompletion and type checking. Read more about generating ontologies with @tomic/cli.

Reading Data

How you read data from a resource depends on whether you've annotated the resource with a class or not.

For annotated resources, it's as easy as using the .props accessor:

import { type Article } from './ontologies/article';
const resource = await store.getResource<Article>(
  'https://my-atomicserver.com/my-resource',
);

console.log(resource.props.category); // string
console.log(resource.props.likesAmount); // number

for non annotated resources you can use the .get method:

import { core } from '@tomic/lib';

const resource = await store.getResource(
  'https://my-atomicserver.com/my-resource',
);

const description = resource.get(core.properties.description); // string | undefined
const category = resource.get(
  'https://my-atomicserver.com/properties/category',
); // JSONValue

Writing Data

Writing data is done using the .set method.

import { core } from '@tomic/lib';

// With Validation
await resource.set(core.properties.description, 'New description');

// Without Validation
resource.set(core.properties.description, 'New description', false);

await resource.save();

By default, .set validates the value against the properties datatype. You should await the method when validation is enabled because the property's resource might not be in the store yet and has to be fetched.

Note
Setting validate to false only disables validation on the client. The server will always validate the data and respond with an error if the data is invalid.

Parameters

NameTypeDescription
propertystringSubject of the property to set
valueJSONValue*The value to set
validatebooleanWhether to validate the value against the property's datatype

*When setting properties from known ontologies, you get automatic type-checking as a bonus.

Pushing to ResourceArrays

You can use the .push method to push data to a ResourceArray property.

import { socialmedia } from '../ontologies/socialmedia';

resource.push(socialmedia.properties.likedBy, [
  'https://my-atomicserver.com/users/1',
]);

// Only add values that are not in the array already
resource.push(
  socialmedia.properties.likedBy,
  ['https://my-atomicserver.com/users/1'],
  true,
);

await resource.save();

Parameters

NameTypeDescription
propertystringSubject of the property to push to
valuesJSONArraylist of values to push
uniqueboolean(Optional) When true, does not push values already contained in the list. (Defaults to false)

Removing properties

Removing properties is done using the .remove method. Alternatively, you can pass undefined as a value to .set

import { core } from '@tomic/lib';

resource.remove(core.properties.description);
// or
resource.set(core.properties.description, undefined);

await resource.save();

Saving

When using methods like .set() and .push(), the changes will be collected into a single commit. This commit is only reflected locally and has to be sent to the server to make the changes permanent. To do this, call the .save() method.

await resource.save();

You can check if a resource has unsaved changes with the .hasUnsavedChanges() method.

if (resource.hasUnsavedChanges()) {
  // Show a save button
}

Deleting resources

Deleting resources is done using the .destroy() method.

await resource.destroy();

When a resource is deleted, all children of the resource are also deleted.

Classes

Classes are an essential part of Atomic Data, therefore Resource has a few useful methods for reading and setting classes.

Reading classes

Resource provides a .getClasses method to get the classes of the resource.

const classes = resource.getClasses(); // string[]

// Syntactic sugar for:
// const classes = resource.get(core.properties.isA);

If you just want to know if a resource is of a certain class use: .hasClasses()

if (resource.hasClasses('https://my-atomicserver.com/classes/Article')) {
  //...
}

// multiple classes (AND)
if (resource.hasClasses(core.classes.agent, dataBrowser.classes.folder)) {
  // ...
}

There are often situations where you want the value of some variable to depend on the class of a resource. An example would be a React component that renders different subcomponents based on the resource it is given. The .matchClass() method makes this easy.

// A React component that renders a resource inside a table cell.
function ResourceTableCell({ subject }: ResourceTableCellProps): JSX.Element {
  // A react hook that fetches the resource
  const resource = useResource(subject);

  // Depending on the class of the resource, render a different component
  const Comp = resource.matchClass(
    {
      [core.classes.agent]: AgentCell,
      [server.classes.file]: FileCell,
    },
    BasicCell,
  );

  return <Comp resource={resource} />;
}

.matchClass() takes an object that maps class subjects to values. If the resource has a class that is a key in the object, the corresponding value is returned. An optional fallback value can be provided as the second argument. The order of the classes in the object is important, as the first match is returned.

Writing classes

To add classes to a resource, use the .addClasses() method.

resource.addClasses('https://my-atomicserver.com/classes/Article');

// With multiple classes
resource.addClasses(
  'https://my-atomicserver.com/classes/Article',
  'https://my-atomicserver.com/classes/Comment',
);

Finally, there is the .removeClasses() method to remove classes from a resource.

resource.removeClasses('https://my-atomicserver.com/classes/Article');

Access Rights

Sometimes you want to check if your agent has write access to a resource and maybe render a different UI based on that. To check this use .canWrite().

if (await resource.canWrite()) {
  // Render a UI with edit buttons
}

You can also get a list of all rights for the resource using .getRights().

const rights = await resource.getRights();

History and versions

AtomicServer keeps a record of all changes (commits) done to a resource. This allows you to roll back to anywhere in the history of the resource.

To get a list of each version of the resource use resource.getHistory(). When you've found a version you want to roll back to, use resource.setVersion().

const versions = await resource.getHistory();

const version = userPicksVersion(versions);

await resource.setVersion(version);

Useful methods and properties

Subject

resource.subject is a get accessor that returns the subject of the resource.

Loading & Error states

If an error occurs while fetching the resource, resource.error will be set. Additionally, when a resource is fetched using store.getResourceLoading() a resource is returned immediately that is not yet complete. To check if the resource is not fully loaded yet use resource.loading.

These properties are useful when you want to show a loading spinner or an error message while fetching a resource.

import { useResource } from '@tomic/react';

function ResourceCard({ subject }: ResourceCardProps): JSX.Element {
  // Uses store.getResourceLoading internally.
  const resource = useResource(subject);

  if (resource.error) {
    return <ErrorCard error={resource.error} />;
  }

  if (resource.loading) {
    return <LoadingSpinner />;
  }

  return <Card>{resource.title}</Card>;
}

If you want to know if a resource has errored because the user was not authorised, use resource.isUnauthorized().

Title

resource.title is a get accessor that returns the title of the resource. This title is the value of either the name, shortname or filename of the resource, whichever is present first. Falls back to the subject if none of these are present.

Children

resource.getChildrenCollection() returns a Collection that has all the children of the resource.