Skip to content

Commit 8061945

Browse files
committed
feat: 🎸 allow new block creation through block.upd
1 parent 52b273c commit 8061945

File tree

5 files changed

+78
-4
lines changed

5 files changed

+78
-4
lines changed

src/__demos__/json-crdt-server/routes/block/methods/upd.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ export const upd =
1414
title: 'Patches',
1515
description: 'The patches to apply to the document.',
1616
}),
17+
t.propOpt('create', t.bool).options({
18+
title: 'Create if not exists',
19+
description: 'If true, creates a new document if it does not exist.',
20+
}),
1721
);
1822

1923
const Response = t.Object(
@@ -29,8 +33,8 @@ export const upd =
2933
description: 'Applies patches to an existing document and returns the latest concurrent changes.',
3034
});
3135

32-
return r.prop('block.upd', Func, async ({id, patches}) => {
33-
const res = await services.blocks.edit(id, patches);
36+
return r.prop('block.upd', Func, async ({id, patches, create}) => {
37+
const res = await services.blocks.edit(id, patches, !!create);
3438
const patchesReturn: ResolveType<typeof BlockPatchPartialReturnRef>[] = res.patches.map((patch) => ({
3539
ts: patch.created,
3640
}));

src/__demos__/json-crdt-server/services/blocks/BlocksServices.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,13 @@ export class BlocksServices {
119119
return {patches};
120120
}
121121

122-
public async edit(id: string, patches: Pick<StorePatch, 'blob'>[]) {
122+
public async edit(id: string, patches: Pick<StorePatch, 'blob'>[], createIfNotExists: boolean) {
123+
if (createIfNotExists) {
124+
const exists = await this.store.exists(id);
125+
if (!exists) {
126+
return await this.create(id, patches);
127+
}
128+
}
123129
this.maybeGc();
124130
if (!Array.isArray(patches)) throw RpcError.validation('patches must be an array');
125131
if (!patches.length) throw RpcError.validation('patches must not be empty');

src/__demos__/json-crdt-server/services/blocks/MemoryStore.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {Model, Patch} from 'json-joy/lib/json-crdt';
22
import type * as types from './types';
3+
import {RpcError} from '../../../../common/rpc/caller';
34

45
const tick = new Promise((resolve) => setImmediate(resolve));
56

@@ -14,6 +15,11 @@ export class MemoryStore implements types.Store {
1415
return {snapshot};
1516
}
1617

18+
public async exists(id: string): Promise<boolean> {
19+
await tick;
20+
return this.snapshots.has(id);
21+
}
22+
1723
public async seq(id: string): Promise<number | undefined> {
1824
await tick;
1925
return this.snapshots.get(id)?.seq;
@@ -32,7 +38,7 @@ export class MemoryStore implements types.Store {
3238
if (!Array.isArray(patches) || !patches.length) throw new Error('NO_PATCHES');
3339
const snapshot = this.snapshots.get(id);
3440
const existingPatches = this.patches.get(id);
35-
if (!snapshot || !existingPatches) throw new Error('BLOCK_NOT_FOUND');
41+
if (!snapshot || !existingPatches) throw RpcError.notFound();
3642
let seq = patches[0].seq;
3743
const diff = seq - snapshot.seq - 1;
3844
if (snapshot.seq + 1 < seq) throw new Error('PATCH_SEQ_TOO_HIGH');

src/__tests__/json-crdt-server/block.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,60 @@ export const runBlockTests = (_setup: ApiTestSetup, params: {staticOnly?: true}
114114
});
115115

116116
describe('block.upd', () => {
117+
test('can create a new block', async () => {
118+
const {call, stop} = await setup();
119+
const id = getId();
120+
const model = Model.withLogicalClock();
121+
model.api.root({
122+
text: 'Hell',
123+
});
124+
const patch1 = model.api.flush();
125+
const result = await call('block.upd', {
126+
create: true,
127+
id,
128+
patches: [
129+
{
130+
blob: patch1.toBinary(),
131+
},
132+
],
133+
});
134+
expect(result).toMatchObject({
135+
patches: [
136+
{
137+
ts: expect.any(Number),
138+
},
139+
],
140+
});
141+
stop();
142+
});
143+
144+
test('throws BLOCK_NOT_FOUND when "create" flag missing', async () => {
145+
const {call, stop} = await setup();
146+
const id = getId();
147+
const model = Model.withLogicalClock();
148+
model.api.root({
149+
text: 'Hell',
150+
});
151+
const patch1 = model.api.flush();
152+
try {
153+
const result = await call('block.upd', {
154+
create: false,
155+
id,
156+
patches: [
157+
{
158+
blob: patch1.toBinary(),
159+
},
160+
],
161+
});
162+
throw 'not this error';
163+
} catch (error) {
164+
expect(error).toMatchObject({
165+
code: 'NOT_FOUND',
166+
});
167+
}
168+
stop();
169+
});
170+
117171
test('can edit a document sequentially', async () => {
118172
const {call, stop} = await setup();
119173
const id = getId();

src/common/rpc/caller/error/RpcError.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ export class RpcError extends Error implements IRpcError {
5757
return RpcError.fromCode(RpcErrorCodes.BAD_REQUEST, message);
5858
}
5959

60+
public static notFound(message = 'Not Found'): RpcError {
61+
return RpcError.fromCode(RpcErrorCodes.NOT_FOUND, message);
62+
}
63+
6064
public static validation(message: string, meta?: unknown): RpcError {
6165
return RpcError.fromCode(RpcErrorCodes.BAD_REQUEST, message, meta);
6266
}

0 commit comments

Comments
 (0)