Skip to content

Commit

Permalink
Add more information on properly using transactions in Nymph.
Browse files Browse the repository at this point in the history
  • Loading branch information
hperrin committed Jun 14, 2024
1 parent ac4afb4 commit be301a9
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 14 deletions.
6 changes: 3 additions & 3 deletions src/routes/user-guide/creating-entities/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ if (someBlogPost.guid == null) {
await blogPost.$save();
let superPosts = await nymph.getEntities(
{ class: BlogPost.class },
{ class: BlogPost },
{ type: '&', tag: 'super-post' }
);
Expand Down Expand Up @@ -106,8 +106,8 @@ console.log(entity.foo.bar); // Outputs undefined.`}
Nymph. Instead, it creates instances without data called "sleeping
references". You can use `$wake` on the entity or `$wakeAll` on the parent
entity to get the entity's data from the DB. The <code>$wakeAll</code>
method will awaken all sleeping references in the entity's data. You can
call <code>$clearCache</code>
method will awaken all sleeping references in the entity's data. You can call
<code>$clearCache</code>
in Node.js or <code>$refresh</code> in the client to turn all the entities back
into sleeping references.
</p>
Expand Down
5 changes: 3 additions & 2 deletions src/routes/user-guide/entity-class/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,9 @@ await nymph.deleteEntities([entity]);`}
if it's true, the function uses <code>$equals</code>.
</li>
<li>
<code>$arraySearch</code> - Search an array for the entity and return the
corresponding key. Takes two arguments, the array and a boolean
<code>$arraySearch</code> - Search an array for the entity and return its
index, or <code>-1</code> if it's not found. Takes two arguments, the
array and a boolean
<code>strict</code>. If <code>strict</code> is false or undefined, the
function uses
<code>$is</code>
Expand Down
13 changes: 4 additions & 9 deletions src/routes/user-guide/entity-querying/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,12 @@

<Highlight
language={typescript}
code={`// FoobarBaz expects a GUID.
const baz = await FoobarBaz.factory(guid);
code={`const baz = await FoobarBaz.factory(guid);
if (baz.guid == null) {
console.error("Can't find the Foobar Baz!");
}
// Tilmeld's User class expects a GUID or a username.
// Tilmeld's User class has a username factory function.
const cronUser = await User.factoryUsername('cron');
if (cronUser.guid == null) {
console.error("Can't find the cron user!");
Expand Down Expand Up @@ -62,11 +61,7 @@ if (cronUser.guid == null) {
<td>class</td>
<td>typeof Entity</td>
<td>Entity</td>
<td
>The class used to create each entity. It must have a <code
>factory</code
> static method that returns a new instance.</td
>
<td>The Entity class to query.</td>
</tr>
<tr>
<td>limit</td>
Expand Down Expand Up @@ -247,7 +242,7 @@ if (cronUser.guid == null) {
>The named property contains the value (its JSON string is found
within the property's JSON string).</td
>
<td><code>{"{type: '&', array: ['foo', 'bar']}"}</code></td>
<td><code>{"{type: '&', contain: ['foo', 'bar']}"}</code></td>
<td><code>{"entity.foo = ['bar', 'baz']"}</code></td>
</tr>
<tr>
Expand Down
154 changes: 154 additions & 0 deletions src/routes/user-guide/transactions/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<svelte:head>
<title>Transactions - User Guide - Nymph.js</title>
{@html github}
</svelte:head>

<section>
Expand Down Expand Up @@ -41,6 +42,12 @@
instance. It will be tied to a specific connection to the database.
</p>

<p>
The transaction instance of Nymph has its own set of classes. You can use
its <code>getEntityClass</code> method to get the proper classes for that instance
of Nymph.
</p>

<p>
When you start a new transaction, entities retrieved from that transaction's
Nymph instance will have that instance within their static <code>nymph</code
Expand All @@ -56,6 +63,149 @@
transactions. To ensure data consistency, it's highly recommended to use a
configuration that supports transactions.
</p>

<p>
Here is an example of a class that uses a transaction to delete all of its
children when it is deleted. If any of its children cannot be deleted, then
the transaction is rolled back, meaning none of its children get deleted.
</p>

<Highlight
language={typescript}
code={`import { EntityUniqueConstraintError, type Nymph } from '@nymphjs/nymph';
import { Entity, nymphJoiProps } from '@nymphjs/nymph';
import type { AccessControlData } from '@nymphjs/tilmeld';
import { enforceTilmeld, tilmeldJoiProps } from '@nymphjs/tilmeld';
import Joi from 'joi';
export type TodoData = {
name: string;
done: boolean;
parent?: Todo & TodoData;
} & AccessControlData;
export class Todo extends Entity<TodoData> {
static ETYPE = 'todo';
static class = 'Todo';
protected $clientEnabledMethods = [];
protected $allowlistData? = ['name', 'done', 'parent'];
protected $protectedTags = [];
protected $allowlistTags? = [];
constructor() {
super();
this.$data.name = '';
this.$data.done = false;
}
async $getUniques() {
return [
\`\${this.$data.user?.guid}:\${this.$data.parent?.guid}:\${this.$data.name}\`,
];
}
/**
* Set a new Nymph instance on this and all contained entities.
*/
$setNymph(nymph: Nymph) {
this.$nymph = nymph;
if (!this.$asleep()) {
if (this.$data.user) {
this.$data.user.$nymph = nymph;
}
if (this.$data.group) {
this.$data.group.$nymph = nymph;
}
if (this.$data.parent) {
this.$data.parent.$setNymph(nymph);
}
}
}
async $save() {
const tilmeld = enforceTilmeld(this);
if (!tilmeld.gatekeeper()) {
// Only allow logged in users to save.
throw new Error('You are not logged in.');
}
// Validate the entity's data.
Joi.attempt(
this.$getValidatable(),
Joi.object().keys({
...nymphJoiProps,
...tilmeldJoiProps,
name: Joi.string().trim(false).max(500, 'utf8').required(),
done: Joi.boolean().required(),
parent: Joi.object().instance(Todo),
}),
'Invalid Todo: ',
);
try {
return await super.$save();
} catch (e: any) {
if (e instanceof EntityUniqueConstraintError) {
throw new Error('There is already a todo for that.');
}
throw e;
}
}
async $delete() {
const transaction = 'todo-delete-' + this.guid;
const nymph = this.$nymph;
const tnymph = await nymph.startTransaction(transaction);
this.$setNymph(tnymph);
try {
// Delete this todo's children.
const children = await tnymph.getEntities(
{
class: tnymph.getEntityClass(Todo),
skipAc: true,
},
{
type: '&',
ref: ['parent', this],
},
);
for (let child of children) {
if (!(await child.$delete())) {
throw new Error("Couldn't delete child todo.");
}
}
// Delete todo.
let success = await super.$delete();
if (success) {
success = await tnymph.commit(transaction);
} else {
await tnymph.rollback(transaction);
}
this.$setNymph(nymph);
return success;
} catch (e: any) {
await tnymph.rollback(transaction);
this.$setNymph(nymph);
throw e;
}
}
}
`}
/>

<p>
The <code>$setNymph</code> method is used to make sure the entity and all
referenced entities use the transactional Nymph instance. The
<code>tnymph</code> Nymph instance is used during the transaction, and the
children are retrieved using the proper class with
<code>tnymph.getEntityClass(Todo)</code>.
</p>
</section>

<section class="page-steps">
Expand All @@ -70,5 +220,9 @@
</section>

<script lang="ts">
import Highlight from 'svelte-highlight';
import typescript from 'svelte-highlight/languages/typescript';
import github from 'svelte-highlight/styles/github';
import { base } from '$app/paths';
</script>

0 comments on commit be301a9

Please sign in to comment.