Skip to content

Commit 6a2351f

Browse files
committed
Add schema support
1 parent 7c7f974 commit 6a2351f

File tree

13 files changed

+568
-21
lines changed

13 files changed

+568
-21
lines changed

packages/schema/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@yorkie-js/schema",
3-
"version": "0.6.6",
3+
"version": "0.6.7-rc",
44
"description": "Yorkie Schema for Yorkie Document",
55
"main": "./src/index.ts",
66
"publishConfig": {
File renamed without changes.

packages/schema/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@
1515
*/
1616

1717
export { validate } from './validator';
18+
export { buildRuleset } from './rulesets';

packages/schema/src/rulesets.ts

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
* Copyright 2025 The Yorkie Authors. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { CharStreams, CommonTokenStream } from 'antlr4ts';
18+
import { ParseTreeWalker } from 'antlr4ts/tree';
19+
import {
20+
PrimitiveTypeContext,
21+
PropertyNameContext,
22+
TypeAliasDeclarationContext,
23+
YorkieSchemaParser,
24+
} from '../antlr/YorkieSchemaParser';
25+
import { YorkieSchemaLexer } from '../antlr/YorkieSchemaLexer';
26+
import { YorkieSchemaListener } from '../antlr/YorkieSchemaListener';
27+
28+
/**
29+
* `Rule` represents a rule for a field in the schema.
30+
*/
31+
export type Rule = StringRule | ObjectRule | ArrayRule;
32+
export type RuleType = 'string' | 'object' | 'array';
33+
34+
export type RuleBase = {
35+
path: string;
36+
type: RuleType;
37+
};
38+
39+
export type StringRule = {
40+
type: 'string';
41+
} & RuleBase;
42+
43+
export type ObjectRule = {
44+
type: 'object';
45+
properties: { [key: string]: RuleBase };
46+
} & RuleBase;
47+
48+
export type ArrayRule = {
49+
type: 'array';
50+
} & RuleBase;
51+
52+
/**
53+
* `RulesetBuilder` is a visitor that builds a ruleset from the given schema.
54+
*/
55+
export class RulesetBuilder implements YorkieSchemaListener {
56+
private currentPath: Array<string> = ['$'];
57+
private ruleMap: Map<string, Rule> = new Map();
58+
59+
/**
60+
* `enterTypeAliasDeclaration` is called when entering a type alias declaration.
61+
*/
62+
enterTypeAliasDeclaration(ctx: TypeAliasDeclarationContext) {
63+
const typeName = ctx.Identifier().text;
64+
if (typeName === 'Document') {
65+
this.currentPath = ['$'];
66+
}
67+
}
68+
69+
/**
70+
* `enterPropertyName` is called when entering a property name.
71+
*/
72+
enterPropertyName(ctx: PropertyNameContext) {
73+
const propName = ctx.Identifier()!.text;
74+
this.currentPath.push(propName);
75+
}
76+
77+
/**
78+
* `enterPrimitiveType` is called when entering a primitive type.
79+
*/
80+
enterPrimitiveType(ctx: PrimitiveTypeContext) {
81+
const type = ctx.text;
82+
const path = this.buildPath();
83+
const rule = {
84+
path,
85+
type,
86+
} as Rule;
87+
88+
this.ruleMap.set(path, rule);
89+
this.currentPath.pop();
90+
}
91+
92+
private buildPath(): string {
93+
return this.currentPath.join('.');
94+
}
95+
96+
/**
97+
* `build` returns the built ruleset.
98+
*/
99+
build(): Map<string, Rule> {
100+
return this.ruleMap;
101+
}
102+
}
103+
104+
/**
105+
* `buildRuleset` builds a ruleset from the given schema string.
106+
*/
107+
export function buildRuleset(schema: string): Map<string, Rule> {
108+
const stream = CharStreams.fromString(schema);
109+
const lexer = new YorkieSchemaLexer(stream);
110+
const tokens = new CommonTokenStream(lexer);
111+
const parser = new YorkieSchemaParser(tokens);
112+
const tree = parser.document();
113+
const builder = new RulesetBuilder();
114+
ParseTreeWalker.DEFAULT.walk(builder as any, tree);
115+
return builder.build();
116+
}

packages/schema/test/ruleset.test.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright 2025 The Yorkie Authors. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { describe, it, expect } from 'vitest';
18+
import { buildRuleset } from '../src/rulesets';
19+
20+
describe('RulesetBuilder', () => {
21+
it('should create rules for simple document', () => {
22+
const schema = `
23+
type Document = {
24+
name: string;
25+
age: number;
26+
};
27+
`;
28+
29+
const ruleset = buildRuleset(schema);
30+
expect(ruleset.get('$.name')!.type).to.eq('string');
31+
expect(ruleset.get('$.age')!.type).to.eq('number');
32+
});
33+
34+
it('should handle nested objects', () => {
35+
const schema = `
36+
type Document = {
37+
user: User;
38+
};
39+
40+
type User = {
41+
name: string;
42+
address: Address;
43+
};
44+
45+
type Address = {
46+
street: string;
47+
city: string;
48+
};
49+
`;
50+
51+
const ruleset = buildRuleset(schema);
52+
expect(ruleset.get('$.user.name')!.type).to.eq('string');
53+
expect(ruleset.get('$.user.address.street')!.type).to.eq('string');
54+
expect(ruleset.get('$.user.address.city')!.type).to.eq('string');
55+
});
56+
57+
// TODO(hackerwins): Implement array type handling.
58+
it.todo('should handle array types', () => {
59+
const schema = `
60+
type Document = {
61+
todos: Array<Todo>;
62+
};
63+
64+
type Todo = {
65+
id: string;
66+
text: string;
67+
};
68+
`;
69+
70+
const ruleset = buildRuleset(schema);
71+
expect(ruleset.get('$.todos')!.type).to.eq('array');
72+
expect(ruleset.get('$.todos[*].id')!.type).to.be('string');
73+
expect(ruleset.get('$.todos[*].text')!.type).to.be('string');
74+
});
75+
});

packages/sdk/public/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,7 @@ <h4 class="title">
338338

339339
await client.attach(doc, {
340340
initialPresence: { color: getRandomColor() },
341+
schema: 'test@1',
341342
});
342343

343344
doc.update((root) => {

packages/sdk/src/api/yorkie/v1/resources.proto

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ message ChangePack {
4747
TimeTicket min_synced_ticket = 5;
4848
bool is_removed = 6;
4949
VersionVector version_vector = 7;
50+
repeated Rule rules = 8;
5051
}
5152

5253
message Change {
@@ -286,8 +287,9 @@ message TreePos {
286287

287288
message User {
288289
string id = 1;
289-
string username = 2;
290-
google.protobuf.Timestamp created_at = 3;
290+
string auth_provider = 2;
291+
string username = 3;
292+
google.protobuf.Timestamp created_at = 4;
291293
}
292294

293295
message Project {
@@ -297,26 +299,50 @@ message Project {
297299
string secret_key = 4;
298300
string auth_webhook_url = 5;
299301
repeated string auth_webhook_methods = 6;
300-
string client_deactivate_threshold = 7;
301-
google.protobuf.Timestamp created_at = 8;
302-
google.protobuf.Timestamp updated_at = 9;
302+
string event_webhook_url = 7;
303+
repeated string event_webhook_events = 8;
304+
string client_deactivate_threshold = 9;
305+
int32 max_subscribers_per_document = 10;
306+
int32 max_attachments_per_document = 11;
307+
repeated string allowed_origins = 14;
308+
google.protobuf.Timestamp created_at = 12;
309+
google.protobuf.Timestamp updated_at = 13;
310+
}
311+
312+
message MetricPoint {
313+
int64 timestamp = 1;
314+
int32 value = 2;
303315
}
304316

305317
message UpdatableProjectFields {
306318
message AuthWebhookMethods {
307319
repeated string methods = 1;
308320
}
309321

322+
message EventWebhookEvents {
323+
repeated string events = 1;
324+
}
325+
326+
message AllowedOrigins {
327+
repeated string origins = 1;
328+
}
329+
310330
google.protobuf.StringValue name = 1;
311331
google.protobuf.StringValue auth_webhook_url = 2;
312332
AuthWebhookMethods auth_webhook_methods = 3;
313-
google.protobuf.StringValue client_deactivate_threshold = 4;
333+
google.protobuf.StringValue event_webhook_url = 4;
334+
EventWebhookEvents event_webhook_events = 5;
335+
google.protobuf.StringValue client_deactivate_threshold = 6;
336+
google.protobuf.Int32Value max_subscribers_per_document = 7;
337+
google.protobuf.Int32Value max_attachments_per_document = 8;
338+
AllowedOrigins allowed_origins = 9;
314339
}
315340

316341
message DocumentSummary {
317342
string id = 1;
318343
string key = 2;
319344
string snapshot = 3;
345+
int32 attached_clients = 7;
320346
google.protobuf.Timestamp created_at = 4;
321347
google.protobuf.Timestamp accessed_at = 5;
322348
google.protobuf.Timestamp updated_at = 6;
@@ -388,3 +414,17 @@ message DocEvent {
388414
string publisher = 2;
389415
DocEventBody body = 3;
390416
}
417+
418+
message Schema {
419+
string id = 1;
420+
string name = 2;
421+
int32 version = 3;
422+
string body = 4;
423+
repeated Rule rules = 5;
424+
google.protobuf.Timestamp created_at = 6;
425+
}
426+
427+
message Rule {
428+
string path = 1;
429+
string type = 2;
430+
}

0 commit comments

Comments
 (0)