Skip to content

Commit 051006d

Browse files
MXPOLIdokah
authored andcommitted
Capabilities property in collection object (#381)
* feat: capabilities property in collection object * refactor: some refactors * refactor: refactors based on review * refactor: some refactors based on reviews * refactor: rename some variables and methods
1 parent c34860d commit 051006d

File tree

17 files changed

+339
-250
lines changed

17 files changed

+339
-250
lines changed
Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
1-
export const hasSameSchemaFieldsLike = (fields: {field: string, [x: string]: any}[]) => expect.arrayContaining( fields.map((f: any) => expect.objectContaining( f ) ))
1+
import { SystemFields } from '@wix-velo/velo-external-db-commons'
2+
import { ResponseField } from '@wix-velo/velo-external-db-types'
23

3-
export const collectionWithDefaultFields = () => hasSameSchemaFieldsLike([ { field: '_id', type: 'text' },
4-
{ field: '_createdDate', type: 'datetime' },
5-
{ field: '_updatedDate', type: 'datetime' },
6-
{ field: '_owner', type: 'text' } ])
4+
export const hasSameSchemaFieldsLike = (fields: ResponseField[]) => expect.arrayContaining(fields.map((f) => expect.objectContaining( f )))
5+
6+
export const toContainDefaultFields = () => hasSameSchemaFieldsLike(SystemFields.map(f => ({ field: f.name, type: f.type })))
7+
8+
export const collectionToContainFields = (collectionName: string, fields: ResponseField[], capabilities: any) => ({
9+
id: collectionName,
10+
fields: hasSameSchemaFieldsLike(fields),
11+
capabilities: {
12+
collectionOperations: capabilities.CollectionOperations,
13+
dataOperations: capabilities.ReadWriteOperations,
14+
fieldTypes: capabilities.FieldTypes
15+
}
16+
})
17+
18+
export const toBeDefaultCollectionWith = (collectionName: string, capabilities: any) => collectionToContainFields(collectionName, SystemFields.map(f => ({ field: f.name, type: f.type })), capabilities)

apps/velo-external-db/test/resources/provider_resources.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,13 @@ export const env: {
3131
schemaProvider: ISchemaProvider
3232
cleanup: ConnectionCleanUp
3333
driver: AnyFixMe
34+
capabilities: any
3435
} = {
3536
dataProvider: Uninitialized,
3637
schemaProvider: Uninitialized,
3738
cleanup: Uninitialized,
3839
driver: Uninitialized,
40+
capabilities: Uninitialized,
3941
}
4042

4143
const dbInit = async(impl: any) => {
@@ -48,6 +50,7 @@ const dbInit = async(impl: any) => {
4850
env.dataProvider = new impl.DataProvider(pool, driver.filterParser)
4951
env.schemaProvider = new impl.SchemaProvider(pool, testResources.schemaProviderTestVariables?.() )
5052
env.driver = driver
53+
env.capabilities = impl.testResources.capabilities
5154
env.cleanup = cleanup
5255
}
5356

@@ -56,6 +59,7 @@ export const dbTeardown = async() => {
5659
env.dataProvider = Uninitialized
5760
env.schemaProvider = Uninitialized
5861
env.driver = Uninitialized
62+
env.capabilities = Uninitialized
5963
}
6064

6165
const postgresTestEnvInit = async() => await dbInit(postgres)
@@ -70,7 +74,7 @@ const bigqueryTestEnvInit = async() => await dbInit(bigquery)
7074
const googleSheetTestEnvInit = async() => await dbInit(googleSheet)
7175

7276
const testSuits = {
73-
mysql: suiteDef('MySql', mysqlTestEnvInit, mysql.testResources.supportedOperations),
77+
mysql: suiteDef('MySql', mysqlTestEnvInit, mysql.testResources),
7478
postgres: suiteDef('Postgres', postgresTestEnvInit, postgres.testResources.supportedOperations),
7579
spanner: suiteDef('Spanner', spannerTestEnvInit, spanner.testResources.supportedOperations),
7680
firestore: suiteDef('Firestore', firestoreTestEnvInit, firestore.testResources.supportedOperations),
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
1-
export const suiteDef = (name: string, setup: any, supportedOperations: any) => ( { name, setup, supportedOperations } )
1+
export const suiteDef = (name: string, setup: any, testResources: any) => ({
2+
name,
3+
setup,
4+
supportedOperations: testResources.supportedOperations,
5+
capabilities: testResources.capabilities
6+
})

apps/velo-external-db/test/storage/schema_provider.spec.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { errors, SystemFields } from '@wix-velo/velo-external-db-commons'
33
import { SchemaOperations } from '@wix-velo/velo-external-db-types'
44
import { Uninitialized, gen, testIfSupportedOperationsIncludes } from '@wix-velo/test-commons'
55
import { env, dbTeardown, setupDb, currentDbImplementationName, supportedOperations } from '../resources/provider_resources'
6-
import { collectionWithDefaultFields, hasSameSchemaFieldsLike } from '../drivers/schema_provider_matchers'
6+
import { toContainDefaultFields, collectionToContainFields, toBeDefaultCollectionWith, hasSameSchemaFieldsLike } from '../drivers/schema_provider_matchers'
77
const chance = new Chance()
88
const { CollectionDoesNotExists, FieldAlreadyExists, CannotModifySystemField, FieldDoesNotExist } = errors
99
const { RemoveColumn } = SchemaOperations
@@ -39,19 +39,19 @@ describe(`Schema API: ${currentDbImplementationName()}`, () => {
3939
await expect( env.schemaProvider.list() ).resolves.toEqual(expect.arrayContaining([
4040
expect.objectContaining({
4141
id: ctx.collectionName,
42-
fields: collectionWithDefaultFields()
42+
fields: toContainDefaultFields()
4343
}),
4444
expect.objectContaining({
4545
id: ctx.anotherCollectionName,
46-
fields: collectionWithDefaultFields()
46+
fields: toContainDefaultFields()
4747
})
4848
]))
4949
})
5050

5151
test('create collection with default columns', async() => {
5252
await env.schemaProvider.create(ctx.collectionName)
5353

54-
await expect( env.schemaProvider.describeCollection(ctx.collectionName) ).resolves.toEqual(collectionWithDefaultFields())
54+
await expect( env.schemaProvider.describeCollection(ctx.collectionName) ).resolves.toEqual(toBeDefaultCollectionWith(ctx.collectionName, env.capabilities))
5555
})
5656

5757
test('drop collection', async() => {
@@ -65,13 +65,13 @@ describe(`Schema API: ${currentDbImplementationName()}`, () => {
6565
test('collection name and variables are case sensitive', async() => {
6666
await env.schemaProvider.create(ctx.collectionName.toUpperCase())
6767

68-
await expect( env.schemaProvider.describeCollection(ctx.collectionName.toUpperCase()) ).resolves.toEqual(collectionWithDefaultFields())
68+
await expect( env.schemaProvider.describeCollection(ctx.collectionName.toUpperCase()) ).resolves.toEqual(toBeDefaultCollectionWith(ctx.collectionName.toUpperCase(), env.capabilities))
6969
})
7070

7171
test('retrieve collection data by collection name', async() => {
7272
await env.schemaProvider.create(ctx.collectionName)
7373

74-
await expect( env.schemaProvider.describeCollection(ctx.collectionName) ).resolves.toEqual(collectionWithDefaultFields())
74+
await expect( env.schemaProvider.describeCollection(ctx.collectionName) ).resolves.toEqual(toBeDefaultCollectionWith(ctx.collectionName, env.capabilities))
7575
})
7676

7777
test('create collection twice will do nothing', async() => {
@@ -87,7 +87,7 @@ describe(`Schema API: ${currentDbImplementationName()}`, () => {
8787
test('add column on a an existing collection', async() => {
8888
await env.schemaProvider.create(ctx.collectionName, [])
8989
await env.schemaProvider.addColumn(ctx.collectionName, { name: ctx.columnName, type: 'datetime', subtype: 'timestamp' })
90-
await expect( env.schemaProvider.describeCollection(ctx.collectionName) ).resolves.toEqual( hasSameSchemaFieldsLike([{ field: ctx.columnName }]))
90+
await expect( env.schemaProvider.describeCollection(ctx.collectionName) ).resolves.toEqual(collectionToContainFields(ctx.collectionName, [{ field: ctx.columnName, type: 'datetime' }], env.capabilities))
9191
})
9292

9393
test('add duplicate column will fail', async() => {
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import {
2+
CollectionOperation,
3+
DataOperation,
4+
FieldType,
5+
} from '@wix-velo/velo-external-db-types'
6+
7+
const {
8+
query,
9+
count,
10+
queryReferenced,
11+
aggregate,
12+
} = DataOperation
13+
14+
export const ReadWriteOperations = Object.values(DataOperation)
15+
export const ReadOnlyOperations = [query, count, queryReferenced, aggregate]
16+
export const FieldTypes = Object.values(FieldType)
17+
export const CollectionOperations = Object.values(CollectionOperation)

libs/external-db-mysql/src/mysql_schema_provider.ts

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import { escapeId, escapeTable, columnCapabilitiesFor } from './mysql_utils'
55
import { SystemFields, validateSystemFields, parseTableData, AllSchemaOperations } from '@wix-velo/velo-external-db-commons'
66
import { Pool as MySqlPool } from 'mysql'
77
import { MySqlQuery } from './types'
8-
import { InputField, ISchemaProvider, ResponseField, SchemaOperations, Table, ColumnCapabilities, CollectionCapabilities } from '@wix-velo/velo-external-db-types'
9-
8+
import { InputField, ISchemaProvider, ResponseField, SchemaOperations, Table, CollectionCapabilities } from '@wix-velo/velo-external-db-types'
9+
import { CollectionOperations, FieldTypes, ReadOnlyOperations, ReadWriteOperations } from './mysql_capabilities'
1010

1111
export default class SchemaProvider implements ISchemaProvider {
1212
pool: MySqlPool
@@ -24,10 +24,12 @@ export default class SchemaProvider implements ISchemaProvider {
2424
const currentDb = this.pool.config.connectionConfig.database
2525
const data = await this.query('SELECT TABLE_NAME as table_name, COLUMN_NAME as field, DATA_TYPE as type FROM information_schema.columns WHERE TABLE_SCHEMA = ? ORDER BY TABLE_NAME, ORDINAL_POSITION', currentDb)
2626
const tables: {[x:string]: {table_name: string, field: string, type: string}[]} = parseTableData( data )
27+
2728
return Object.entries(tables)
2829
.map(([collectionName, rs]) => ({
2930
id: collectionName,
30-
fields: rs.map( this.translateDbTypes.bind(this) )
31+
fields: rs.map(this.appendAdditionalRowDetails.bind(this)),
32+
capabilities: this.collectionCapabilities(rs.map(r => r.field))
3133
} ))
3234
}
3335

@@ -74,27 +76,33 @@ export default class SchemaProvider implements ISchemaProvider {
7476
.catch( err => translateErrorCodes(err, collectionName) )
7577
}
7678

77-
async describeCollection(collectionName: string): Promise<ResponseField[]> {
78-
const res = await this.query(`DESCRIBE ${escapeTable(collectionName)}`)
79-
.catch( err => translateErrorCodes(err, collectionName) )
80-
return res.map((r: { Field: string; Type: string }) => ({ field: r.Field, type: r.Type }))
81-
.map( this.translateDbTypes.bind(this) )
79+
async describeCollection(collectionName: string): Promise<Table> {
80+
interface describeTableResponse {
81+
Field: string,
82+
Type: string,
83+
}
84+
85+
const res: describeTableResponse[] = await this.query(`DESCRIBE ${escapeTable(collectionName)}`)
86+
.catch( err => translateErrorCodes(err, collectionName) )
87+
const fields = res.map(r => ({ field: r.Field, type: r.Type })).map(this.appendAdditionalRowDetails.bind(this))
88+
return {
89+
id: collectionName,
90+
fields: fields as ResponseField[],
91+
capabilities: this.collectionCapabilities(res.map(f => f.Field))
92+
}
8293
}
8394

84-
translateDbTypes(row: ResponseField): ResponseField {
95+
private appendAdditionalRowDetails(row: ResponseField) {
8596
row.type = this.sqlSchemaTranslator.translateType(row.type)
97+
row.capabilities = columnCapabilitiesFor(row.type)
8698
return row
8799
}
88100

89-
columnCapabilitiesFor(columnType: string): ColumnCapabilities {
90-
return columnCapabilitiesFor(columnType)
91-
}
92-
93-
capabilities(): CollectionCapabilities {
101+
private collectionCapabilities(fieldNames: string[]): CollectionCapabilities {
94102
return {
95-
dataOperations: [],
96-
fieldTypes: [],
97-
collectionOperations: [],
103+
dataOperations: fieldNames.includes('_id') ? ReadWriteOperations : ReadOnlyOperations,
104+
fieldTypes: FieldTypes,
105+
collectionOperations: CollectionOperations,
98106
}
99107
}
100108
}

libs/external-db-mysql/tests/e2e-testkit/mysql_resources.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { waitUntil } from 'async-wait-until'
33
import init from '../../src/connection_provider'
44
export { supportedOperations } from '../../src/supported_operations'
55

6+
export * as capabilities from '../../src/mysql_capabilities'
67

78
export const connection = () => {
89
const { connection, schemaProvider, cleanup } = init({ host: 'localhost', user: 'test-user', password: 'password', db: 'test-db' }, { connectionLimit: 1, queueLimit: 0 })

libs/velo-external-db-core/src/service/schema.spec.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,12 @@ const chance = Chance()
2323
describe('Schema Service', () => {
2424
describe('Collection new SPI', () => {
2525
test('retrieve all collections from provider', async() => {
26-
const collectionCapabilities = {
27-
dataOperations: [],
28-
fieldTypes: [],
29-
collectionOperations: [],
30-
}
31-
3226
driver.givenAllSchemaOperations()
33-
driver.givenCollectionCapabilities(collectionCapabilities)
3427
driver.givenColumnCapabilities()
3528
driver.givenListResult(ctx.dbsWithIdColumn)
3629

3730

38-
await expect( env.schemaService.list([]) ).resolves.toEqual(collectionsListFor(ctx.dbsWithIdColumn, collectionCapabilities))
31+
await expect( env.schemaService.list([]) ).resolves.toEqual(collectionsListFor(ctx.dbsWithIdColumn))
3932
})
4033

4134
test('create new collection without fields', async() => {
@@ -162,7 +155,6 @@ describe('Schema Service', () => {
162155
// TODO: create a test for the case
163156
// test('collections without _id column will have read-only capabilities', async() => {})
164157

165-
//TODO: create a test for the case
166158
test('run unsupported operations should throw', async() => {
167159
schema.expectSchemaRefresh()
168160
driver.givenAdapterSupportedOperationsWith(ctx.invalidOperations)
@@ -179,7 +171,7 @@ describe('Schema Service', () => {
179171

180172
await expect(env.schemaService.update({ id: ctx.collectionName, fields: [field] })).rejects.toThrow(errors.UnsupportedOperation)
181173

182-
driver.givenFindResults([ { id: ctx.collectionName, fields: [field] }])
174+
driver.givenFindResults([ { id: ctx.collectionName, fields: [{ field: ctx.column.name, type: 'text' }] }])
183175

184176
await expect(env.schemaService.update({ id: ctx.collectionName, fields: [] })).rejects.toThrow(errors.UnsupportedOperation)
185177
await expect(env.schemaService.update({ id: ctx.collectionName, fields: [changedTypeField] })).rejects.toThrow(errors.UnsupportedOperation)

libs/velo-external-db-core/src/service/schema.ts

Lines changed: 22 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ import {
1212
fieldTypeToWixDataEnum,
1313
WixFormatFieldsToInputFields,
1414
responseFieldToWixFormat,
15-
compareColumnsInDbAndRequest
15+
compareColumnsInDbAndRequest,
16+
dataOperationsToWixDataQueryOperators,
17+
collectionOperationsToWixDataCollectionOperations,
1618
} from '../utils/schema_utils'
1719

1820

@@ -29,11 +31,11 @@ export default class SchemaService {
2931
async list(collectionIds: string[]): Promise<collectionSpi.ListCollectionsResponsePart> {
3032
const collections = (!collectionIds || collectionIds.length === 0) ?
3133
await this.storage.list() :
32-
await Promise.all(collectionIds.map(async(collectionName: string) => ({ id: collectionName, fields: await this.schemaInformation.schemaFieldsFor(collectionName) })))
34+
await Promise.all(collectionIds.map((collectionName: string) => this.schemaInformation.schemaFor(collectionName)))
3335

34-
return {
35-
collection: collections.map(this.formatCollection.bind(this))
36-
}
36+
return {
37+
collection: collections.map(this.formatCollection.bind(this))
38+
}
3739
}
3840

3941
async create(collection: collectionSpi.Collection): Promise<collectionSpi.CreateCollectionResponse> {
@@ -51,8 +53,8 @@ export default class SchemaService {
5153
}
5254

5355
const collectionColumnsInRequest = collection.fields
54-
const collectionColumnsInDb = await this.storage.describeCollection(collection.id)
55-
56+
const { fields: collectionColumnsInDb } = await this.storage.describeCollection(collection.id) as Table
57+
5658
const {
5759
columnsToAdd,
5860
columnsToRemove,
@@ -83,9 +85,9 @@ export default class SchemaService {
8385
}
8486

8587
async delete(collectionId: string): Promise<collectionSpi.DeleteCollectionResponse> {
86-
const collectionFields = await this.storage.describeCollection(collectionId)
88+
const { fields: collectionFields } = await this.storage.describeCollection(collectionId) as Table
8789
await this.storage.drop(collectionId)
88-
await this.schemaInformation.refresh()
90+
this.schemaInformation.refresh()
8991
return { collection: {
9092
id: collectionId,
9193
fields: responseFieldToWixFormat(collectionFields),
@@ -100,45 +102,30 @@ export default class SchemaService {
100102
}
101103

102104
private formatCollection(collection: Table): collectionSpi.Collection {
103-
// remove in the end of development
104-
if (!this.storage.capabilities || !this.storage.columnCapabilitiesFor) {
105-
throw new Error('Your storage does not support the new collection capabilities API')
106-
}
107-
const capabilities = this.formatCollectionCapabilities(this.storage.capabilities())
108105
return {
109106
id: collection.id,
110107
fields: this.formatFields(collection.fields),
111-
capabilities
108+
capabilities: collection.capabilities? this.formatCollectionCapabilities(collection.capabilities) : undefined
112109
}
113110
}
114111

115112
private formatFields(fields: ResponseField[]): collectionSpi.Field[] {
116-
const fieldCapabilitiesFor = (type: string): collectionSpi.FieldCapabilities => {
117-
// remove in the end of development
118-
if (!this.storage.columnCapabilitiesFor) {
119-
throw new Error('Your storage does not support the new collection capabilities API')
120-
}
121-
const { sortable, columnQueryOperators } = this.storage.columnCapabilitiesFor(type)
122-
return {
123-
sortable,
124-
queryOperators: queriesToWixDataQueryOperators(columnQueryOperators)
125-
}
126-
}
127-
128-
return fields.map((f) => ({
129-
key: f.field,
130-
// TODO: think about how to implement this
113+
return fields.map( field => ({
114+
key: field.field,
131115
encrypted: false,
132-
type: fieldTypeToWixDataEnum(f.type),
133-
capabilities: fieldCapabilitiesFor(f.type)
116+
type: fieldTypeToWixDataEnum(field.type),
117+
capabilities: {
118+
sortable: field.capabilities? field.capabilities.sortable: undefined,
119+
queryOperators: field.capabilities? queriesToWixDataQueryOperators(field.capabilities.columnQueryOperators): undefined
120+
}
134121
}))
135122
}
136123

137124
private formatCollectionCapabilities(capabilities: CollectionCapabilities): collectionSpi.CollectionCapabilities {
138125
return {
139-
dataOperations: capabilities.dataOperations as unknown as collectionSpi.DataOperation[],
140-
fieldTypes: capabilities.fieldTypes as unknown as collectionSpi.FieldType[],
141-
collectionOperations: capabilities.collectionOperations as unknown as collectionSpi.CollectionOperation[],
126+
dataOperations: capabilities.dataOperations.map(dataOperationsToWixDataQueryOperators),
127+
fieldTypes: capabilities.fieldTypes.map(fieldTypeToWixDataEnum),
128+
collectionOperations: capabilities.collectionOperations.map(collectionOperationsToWixDataCollectionOperations),
142129
}
143130
}
144131

0 commit comments

Comments
 (0)