@dxos/echo-db 2.31.1 → 2.31.2-dev.b400b683

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,179 @@
1
+ //
2
+ // Copyright 2022 DXOS.org
3
+ //
4
+
5
+ import chalk from 'chalk';
6
+ import columnify from 'columnify';
7
+ import expect from 'expect';
8
+ import faker from 'faker';
9
+
10
+ import { truncate, truncateKey } from '@dxos/debug';
11
+ import { ObjectModel } from '@dxos/object-model';
12
+
13
+ import { createData, createSchemas, log, SchemaDefWithGenerator, setup } from '../testing';
14
+ import { Database } from './database';
15
+ import { Item } from './item';
16
+ import { Schema, SchemaField, TYPE_SCHEMA } from './schema';
17
+
18
+ enum TestType {
19
+ Org = 'example:type/org',
20
+ Person = 'example:type/person'
21
+ }
22
+
23
+ const schemaDefs: { [schema: string]: SchemaDefWithGenerator } = {
24
+ [TestType.Org]: {
25
+ schema: 'example:type/schema/organization',
26
+ fields: [
27
+ {
28
+ key: 'title',
29
+ required: true,
30
+ generator: () => faker.company.companyName()
31
+ },
32
+ {
33
+ key: 'website',
34
+ required: false,
35
+ generator: () => faker.internet.url()
36
+ },
37
+ {
38
+ key: 'collaborators',
39
+ required: false,
40
+ generator: () => faker.datatype.number().toString()
41
+ }
42
+ ]
43
+ },
44
+ [TestType.Person]: {
45
+ schema: 'example:type/schema/person',
46
+ fields: [
47
+ {
48
+ key: 'title',
49
+ required: true,
50
+ generator: () => `${faker.name.firstName()} ${faker.name.lastName()}`
51
+ }
52
+ ]
53
+ }
54
+ };
55
+
56
+ describe('Schemas', () => {
57
+ it('creation of Schema', async () => setup(async (database) => {
58
+ const [schema] = await createSchemas(database, [schemaDefs[TestType.Org]]);
59
+ expect(schema.schema).toBe(schemaDefs[TestType.Org].schema);
60
+ expect(schema.fields[0].key).toBe('title');
61
+ }));
62
+
63
+ it('add Schema field', async () => setup(async (database) => {
64
+ const [schema] = await createSchemas(database, [schemaDefs[TestType.Org]]);
65
+
66
+ const newField: SchemaField = {
67
+ key: 'location',
68
+ required: true
69
+ };
70
+ await schema.addField(newField);
71
+
72
+ expect(schema.getField('location')).toBeTruthy();
73
+ }));
74
+
75
+ it('add Schema linked field', async () => setup(async (database) => {
76
+ const [orgSchema, personSchema] = await createSchemas(database, Object.values(schemaDefs));
77
+
78
+ const fieldRef: SchemaField = {
79
+ key: 'organization',
80
+ required: false,
81
+ ref: {
82
+ schema: orgSchema.schema,
83
+ field: orgSchema.fields[0].key
84
+ }
85
+ };
86
+ await personSchema.addField(fieldRef);
87
+
88
+ await createData(database, Object.values(schemaDefs), {
89
+ [schemaDefs[TestType.Org].schema]: 8,
90
+ [schemaDefs[TestType.Person].schema]: 16
91
+ });
92
+
93
+ const items = await database.select().exec().entities;
94
+
95
+ [orgSchema, personSchema].forEach(schema => {
96
+ items.forEach(item => {
97
+ expect(schema.validate(item.model)).toBeTruthy();
98
+ });
99
+ });
100
+ }));
101
+
102
+ it('Use schema to validate the fields of an item', () => setup(async (database) => {
103
+ await createSchemas(database, Object.values(schemaDefs));
104
+ await createData(database, Object.values(schemaDefs), {
105
+ [schemaDefs[TestType.Org].schema]: 8,
106
+ [schemaDefs[TestType.Person].schema]: 16
107
+ });
108
+
109
+ const { entities: schemas } = database
110
+ .select({ type: TYPE_SCHEMA })
111
+ .exec();
112
+
113
+ const { entities: orgs } = database
114
+ .select({ type: TestType.Org })
115
+ .exec();
116
+
117
+ const { entities: people } = database
118
+ .select({ type: TestType.Person })
119
+ .exec();
120
+
121
+ [...orgs, ...people].forEach(item => {
122
+ const schemaItem = schemas.find(schema => schema.model.get('schema') === item.type);
123
+ const schema = new Schema(schemaItem!.model);
124
+ expect(schema.validate(item.model)).toBeTruthy();
125
+ });
126
+
127
+ // Log tables.
128
+ schemas.forEach(schema => {
129
+ const type = schema.model.get('schema');
130
+ const { entities: items } = database.select({ type }).exec();
131
+ log(renderItems(schema, items, database));
132
+ });
133
+ }));
134
+ });
135
+
136
+ /**
137
+ * Log the items for the given schema.
138
+ * @param schema
139
+ * @param items
140
+ * @param [party]
141
+ */
142
+ const renderItems = (schema: Item<ObjectModel>, items: Item<ObjectModel>[], database?: Database) => {
143
+ const fields = Object.values(schema.model.get('fields')) as SchemaField[];
144
+ const columns = fields.map(({ key }) => key);
145
+
146
+ const logKey = (id: string) => truncateKey(id, 4);
147
+ const logString = (value: string) => truncate(value, 24, true);
148
+
149
+ const values = items.map(item => {
150
+ return fields.reduce<{ [key: string]: any }>((row, { key, type, ref }) => {
151
+ const value = item.model.get(key);
152
+ switch (type) {
153
+ case 'string': {
154
+ row[key] = chalk.green(logString(value));
155
+ break;
156
+ }
157
+
158
+ case 'ref': {
159
+ if (database) {
160
+ const { field } = ref!;
161
+ const item = database.getItem(value);
162
+ row[key] = chalk.red(logString(item?.model.get(field)));
163
+ } else {
164
+ row[key] = chalk.red(logKey(value));
165
+ }
166
+ break;
167
+ }
168
+
169
+ default: {
170
+ row[key] = value;
171
+ }
172
+ }
173
+
174
+ return row;
175
+ }, { id: chalk.blue(logKey(item.id)) });
176
+ });
177
+
178
+ return columnify(values, { columns: ['id', ...columns] });
179
+ };
@@ -0,0 +1,100 @@
1
+ //
2
+ // Copyright 2022 DXOS.org
3
+ //
4
+
5
+ import { ObjectModel } from '@dxos/object-model';
6
+
7
+ export const TYPE_SCHEMA = 'dxos:type/schema';
8
+
9
+ export type FieldType = 'string' | 'number' | 'boolean' | 'ref'
10
+
11
+ // TODO(burdon): Protobuf definitions.
12
+
13
+ export type SchemaRef = {
14
+ schema: string
15
+ field: string
16
+ }
17
+
18
+ export type SchemaField = {
19
+ key: string
20
+ type?: FieldType
21
+ required: boolean
22
+ ref?: SchemaRef
23
+ }
24
+
25
+ export type SchemaDef = {
26
+ schema: string
27
+ fields: SchemaField[]
28
+ }
29
+
30
+ /**
31
+ * Wrapper for ECHO Item that represents an `ObjectModel` schema.
32
+ */
33
+ export class Schema {
34
+ constructor (
35
+ private readonly _schema: ObjectModel
36
+ ) {}
37
+
38
+ get schema (): string {
39
+ return this._schema.get('schema');
40
+ }
41
+
42
+ get fields (): SchemaField[] {
43
+ return Object.values(this._schema.get('fields') ?? {});
44
+ }
45
+
46
+ getField (key: string): SchemaField | undefined {
47
+ return this.fields.find(field => field.key === key);
48
+ }
49
+
50
+ // TODO(kaplanski): What happens if an item has extra properties?
51
+ validate (model: ObjectModel) {
52
+ this.fields.forEach(field => {
53
+ const value = model.get(field.key);
54
+ if (field.required) {
55
+ if (!value) {
56
+ return false;
57
+ }
58
+ }
59
+
60
+ if (field.type) {
61
+ if (typeof value !== field.type) {
62
+ return false;
63
+ }
64
+ }
65
+
66
+ if (field.ref) {
67
+ // TODO(kaplanski): Should this class have access to all items in the party to validate?
68
+ // Or maybe possible values should be provided?
69
+ }
70
+ });
71
+ return true;
72
+ }
73
+
74
+ // TODO(kaplanski): Should the field be added to each item using the schema in the party? (Empty value?)
75
+ // TODO(kaplanski): Should the type be infered from the first value added?
76
+ async addField (newField: SchemaField) {
77
+ const newFields = [
78
+ ...this.fields,
79
+ newField
80
+ ];
81
+ // TODO(kaplanski): Create a SET mutation to just modify the field (not all fields).
82
+ await this._schema.set('fields', newFields);
83
+ }
84
+
85
+ // TODO(kaplanski): Should editing a field modify all existing items using this schema?
86
+ async editField (currentKey: string, editedField: SchemaField) {
87
+ const newFields = this.fields.map(field => {
88
+ if (field.key === currentKey) {
89
+ return editedField;
90
+ }
91
+ return field;
92
+ });
93
+ await this._schema.set('fields', newFields);
94
+ }
95
+
96
+ async deleteField (key: string) {
97
+ const newFields = this.fields.filter(field => field.key !== key);
98
+ await this._schema.set('fields', newFields);
99
+ }
100
+ }
@@ -3,17 +3,22 @@
3
3
  //
4
4
 
5
5
  import debug from 'debug';
6
+ import faker from 'faker';
6
7
 
7
8
  import { createKeyPair } from '@dxos/crypto';
9
+ import { ModelFactory } from '@dxos/model-factory';
8
10
  import { NetworkManagerOptions } from '@dxos/network-manager';
11
+ import { ObjectModel } from '@dxos/object-model';
9
12
  import { IStorage } from '@dxos/random-access-multi-storage';
10
13
  import { jsonReplacer } from '@dxos/util';
11
14
 
15
+ import { Database, Schema, SchemaDef, SchemaField, TYPE_SCHEMA } from '../api';
16
+ import { createInMemoryDatabase } from '../database';
12
17
  import { ECHO } from '../echo';
13
18
  import { PartyInternal } from '../parties';
14
19
  import { createRamStorage } from '../util';
15
20
 
16
- const log = debug('dxos:echo-db:testing');
21
+ export const log = debug('dxos:echo-db:testing');
17
22
 
18
23
  export const messageLogger = (tag: string) => (message: any) => {
19
24
  log(tag, JSON.stringify(message, jsonReplacer, 2));
@@ -79,3 +84,90 @@ export const inviteTestPeer = async (party: PartyInternal, peer: ECHO): Promise<
79
84
 
80
85
  return peer.joinParty(invitation, async () => Buffer.from('0000'));
81
86
  };
87
+
88
+ export type SchemaFieldWithGenerator = SchemaField & { generator: () => string }
89
+ export type SchemaDefWithGenerator = Omit<SchemaDef, 'fields'> & { fields: SchemaFieldWithGenerator[] };
90
+
91
+ type Callback = (party: Database) => Promise<void>
92
+
93
+ export const setup = async (callback: Callback) => {
94
+ const modelFactory = new ModelFactory().registerModel(ObjectModel);
95
+ const database = await createInMemoryDatabase(modelFactory);
96
+ try {
97
+ await callback(database);
98
+ } finally {
99
+ await database.destroy();
100
+ }
101
+ };
102
+
103
+ /**
104
+ * Create schema items.
105
+ */
106
+ export const createSchemas = async (database: Database, schemas: SchemaDefWithGenerator[]) => {
107
+ log(`Creating schemas: [${schemas.map(({ schema }) => schema).join()}]`);
108
+
109
+ const schemaItems = await Promise.all(schemas.map(({ schema, fields }) => {
110
+ const schemaFields = fields.map(fieldWithGenerator => {
111
+ // eslint-disable-next-line unused-imports/no-unused-vars
112
+ const { generator, ...field } = fieldWithGenerator;
113
+ return field;
114
+ }).flat();
115
+
116
+ return database.createItem({
117
+ model: ObjectModel,
118
+ type: TYPE_SCHEMA,
119
+ props: {
120
+ schema,
121
+ fields: schemaFields
122
+ }
123
+ });
124
+ }));
125
+
126
+ return schemaItems.map(item => new Schema(item.model));
127
+ };
128
+
129
+ /**
130
+ * Create items for a given schema.
131
+ * NOTE: Assumes that referenced items have already been constructed.
132
+ */
133
+ export const createItems = async (database: Database, { schema, fields }: SchemaDefWithGenerator, numItems: number) => {
134
+ log(`Creating items for: ${schema}`);
135
+
136
+ return await Promise.all(Array.from({ length: numItems }).map(async () => {
137
+ const values = fields.map(field => {
138
+ if (field.ref) {
139
+ // Look-up item.
140
+ const { entities: items } = database.select().filter({ type: field.ref.schema }).exec();
141
+ if (items.length) {
142
+ return {
143
+ [field.key]: faker.random.arrayElement(items).id
144
+ };
145
+ }
146
+ } else {
147
+ return {
148
+ [field.key]: field.generator()
149
+ };
150
+ }
151
+
152
+ return undefined;
153
+ }).filter(Boolean);
154
+
155
+ return await database.createItem({
156
+ type: schema,
157
+ props: Object.assign({}, ...values)
158
+ });
159
+ }));
160
+ };
161
+
162
+ /**
163
+ * Create data for all schemas.
164
+ */
165
+ export const createData = async (database: Database, schemas: SchemaDefWithGenerator[], options: { [key: string]: number } = {}) => {
166
+ // Synchronous loop.
167
+ for (const schema of schemas) {
168
+ const count = options[schema.schema] ?? 0;
169
+ if (count) {
170
+ await createItems(database, schema, count);
171
+ }
172
+ }
173
+ };