diff --git a/browser/CHANGELOG.md b/browser/CHANGELOG.md index cb4b675e7..d6b686379 100644 --- a/browser/CHANGELOG.md +++ b/browser/CHANGELOG.md @@ -12,6 +12,10 @@ This changelog covers all three packages, as they are (for now) updated as a who - [#758](https://github.com/atomicdata-dev/atomic-server/issues/758) Fix Relation column forms to close when clicking on the searchbox - Fix server not rebuilding client when files changed. +### @tomic/lib + +- [#798](https://github.com/atomicdata-dev/atomic-server/issues/798) Add `store.newResource()` to make creating new resources more easy. + ## v0.36.2 ### Atomic Browser diff --git a/browser/lib/src/store.test.ts b/browser/lib/src/store.test.ts index 092476517..6bb3e0f75 100644 --- a/browser/lib/src/store.test.ts +++ b/browser/lib/src/store.test.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; import { jest } from '@jest/globals'; -import { Resource, urls, Store } from './index.js'; +import { Resource, urls, Store, core, Core } from './index.js'; describe('Store', () => { it('renders the populate value', async () => { @@ -56,4 +56,28 @@ describe('Store', () => { expect(customFetch.mock.calls).to.have.length(1); }); + + it('creates new resources using store.newResource()', async () => { + const store = new Store({ serverUrl: 'https://myserver.dev' }); + + const resource1 = await store.newResource({ + subject: 'https://myserver.dev/testthing', + parent: 'https://myserver.dev/properties', + isA: core.classes.property, + propVals: { + [core.properties.datatype]: urls.datatypes.slug, + [core.properties.shortname]: 'testthing', + }, + }); + + expect(resource1.props.parent).to.equal('https://myserver.dev/properties'); + expect(resource1.props.datatype).to.equal(urls.datatypes.slug); + expect(resource1.props.shortname).to.equal('testthing'); + expect(resource1.hasClasses(core.classes.property)).to.equal(true); + + const resource2 = await store.newResource(); + + expect(resource2.props.parent).to.equal(store.getServerUrl()); + expect(resource2.get(core.properties.isA)).to.equal(undefined); + }); }); diff --git a/browser/lib/src/store.ts b/browser/lib/src/store.ts index 045a258ec..6896ccae1 100644 --- a/browser/lib/src/store.ts +++ b/browser/lib/src/store.ts @@ -21,6 +21,7 @@ import { core, commits, collections, + JSONValue, } from './index.js'; import { authenticate, fetchWebSocket, startWebsocket } from './websockets.js'; @@ -38,6 +39,17 @@ type AddResourcesOpts = { skipCommitCompare?: boolean; }; +type CreateResourceOptions = { + /** Optional subject of the new resource, if not given the store will generate a random subject */ + subject?: string; + /** Parent the subject belongs to, defaults to the serverUrl */ + parent?: string; + /** Subject(s) of the resources class */ + isA?: string | string[]; + /** Any additional properties the resource should have */ + propVals?: Record; +}; + export interface StoreOpts { /** The default store URL, where to send commits and where to create new instances */ serverUrl?: string; @@ -184,6 +196,39 @@ export class Store { this.notify(resource.__internalObject); } + /** + * A helper function for creating new resources. + * Options take: + * subject (optional) - defaults to random subject, + * parent (optional) - defaults to serverUrl, + * isA (optional), + * properties (optional) - any additional properties to be set on the resource. + */ + public async newResource( + options: CreateResourceOptions = {}, + ): Promise> { + const { subject, parent, isA, propVals } = options; + + const normalizedIsA = Array.isArray(isA) ? isA : [isA]; + const newSubject = subject ?? this.createSubject(normalizedIsA[0], parent); + + const resource = this.getResourceLoading(newSubject, { newResource: true }); + + if (normalizedIsA[0]) { + await resource.addClasses(this, ...(normalizedIsA as string[])); + } + + await resource.set(core.properties.parent, parent ?? this.serverUrl, this); + + if (propVals) { + for (const [key, value] of Object.entries(propVals)) { + await resource.set(key, value, this); + } + } + + return resource; + } + /** Checks if a subject is free to use */ public async checkSubjectTaken(subject: string): Promise { const r = this.resources.get(subject); diff --git a/docs/src/js.md b/docs/src/js.md index 83b15133c..f7fdf0f80 100644 --- a/docs/src/js.md +++ b/docs/src/js.md @@ -13,28 +13,37 @@ npm install @tomic/lib ``` ```ts -// Import the Store -import { Store, Agent, urls } from "@tomic/lib"; +import { Store, Agent, core } from "@tomic/lib"; -const opts = { +// --------- Create a Store ---------. +const store = new Store({ // You can create a secret from the `User settings` page using the AtomicServer UI agent: Agent.fromSecret("my-secret-key"), // Set a default server URL - serverUrl: "https://atomicdata.dev", -} -const store = new Store(opts); - -// Get a resource -const gotResource = store.getResourceLoading(subject); -const atomString = gotResource! - .get(urls.properties.description)! - .toString(); - -// Create & save a new resource -const subject = 'https://atomicdata.dev/test'; -const newResource = new Resource(subject); -await newResource.set(urls.properties.description, 'Hi world'); -newResource.save(store); + serverUrl: "https://my-atomic-server.dev", +}); + +// --------- Get a resource --------- +const gotResource = await store.getResourceAsync(subject); + +const atomString = gotResource.get(core.properties.description) + +// --------- Create & save a new resource --------- +const newResource = await store.newResource({ + subject: 'https://my-atomic-server.dev/test', + propVals: { + [core.properties.description]: 'Hi World :)' + } +}); + +await newResource.save(store); + +// --------- Subscribe to changes (using websockets) --------- +const unsub = store.subscribe('https://my-atomic-server.dev/test', (resource) => { + // This callback is called each time a change is made to the resource client or serverside. + + // Do something with the changed resource... +}) ``` ## Advanced usage