diff --git a/core/ArSyncApi.d.ts b/core/ArSyncApi.d.ts index df58603..3308124 100644 --- a/core/ArSyncApi.d.ts +++ b/core/ArSyncApi.d.ts @@ -1,5 +1,5 @@ declare const _default: { - fetch: (request: object) => Promise<{}>; - syncFetch: (request: object) => Promise<{}>; + fetch: (request: object) => Promise; + syncFetch: (request: object) => Promise; }; export default _default; diff --git a/core/ArSyncModelBase.d.ts b/core/ArSyncModelBase.d.ts index c234b41..8e4c343 100644 --- a/core/ArSyncModelBase.d.ts +++ b/core/ArSyncModelBase.d.ts @@ -1,7 +1,7 @@ interface Request { - api: string; - query: any; + field: string; params?: any; + query?: any; } declare type Path = (string | number)[]; interface Change { @@ -66,6 +66,6 @@ export default abstract class ArSyncModelBase { static _detach(ref: any): void; private static _attach; static setConnectionAdapter(_adapter: Adapter): void; - static waitForLoad(...models: ArSyncModelBase<{}>[]): Promise<{}>; + static waitForLoad(...models: ArSyncModelBase<{}>[]): Promise; } export {}; diff --git a/core/DataType.d.ts b/core/DataType.d.ts index f391957..5a6088b 100644 --- a/core/DataType.d.ts +++ b/core/DataType.d.ts @@ -16,18 +16,24 @@ interface ExtraFieldErrorType { error: 'extraFieldError'; } declare type DataTypeExtractFromQueryHash = '*' extends keyof QueryType ? { - [key in Exclude<(keyof BaseType) | (keyof QueryType), '_meta' | '_params' | '*'>]: (key extends keyof BaseType ? (key extends keyof QueryType ? (QueryType[key] extends true ? DataTypeExtractField : DataTypeFromQuery) : DataTypeExtractField) : ExtraFieldErrorType); + [key in Exclude<(keyof BaseType) | (keyof QueryType), '_meta' | '*'>]: (key extends keyof QueryType ? _DataTypePickField : key extends keyof BaseType ? DataTypeExtractField : ExtraFieldErrorType); } : { - [key in keyof QueryType]: (key extends keyof BaseType ? (QueryType[key] extends true ? DataTypeExtractField : DataTypeFromQuery) : ExtraFieldErrorType); + [key in keyof QueryType]: _DataTypePickField; }; -declare type _DataTypeFromQuery = QueryType extends keyof BaseType | '*' ? DataTypeExtractFieldsFromQuery : QueryType extends Readonly<(keyof BaseType | '*')[]> ? DataTypeExtractFieldsFromQuery> : QueryType extends { - as: string; -} ? { - error: 'type for alias field is not supported'; -} | undefined : DataTypeExtractFromQueryHash; -export declare type DataTypeFromQuery = BaseType extends any[] ? CheckAttributesField[] : null extends BaseType ? CheckAttributesField | null : CheckAttributesField; +declare type _DataTypePickField = SubQuery extends { + field: infer N; + query?: infer Q; +} ? (N extends keyof BaseType ? (IsAnyCompareLeftType extends Q ? DataTypeExtractField : DataTypeFromQuery) : ExtraFieldErrorType) : (Key extends keyof BaseType ? (SubQuery extends true | { + query?: true | never; + params: any; +} ? DataTypeExtractField : DataTypeFromQuery) : ExtraFieldErrorType); +declare type _DataTypeFromQuery = QueryType extends keyof BaseType | '*' ? DataTypeExtractFieldsFromQuery : QueryType extends Readonly<(keyof BaseType | '*')[]> ? DataTypeExtractFieldsFromQuery> : DataTypeExtractFromQueryHash; +declare type CheckIsArray = BaseType extends (infer Type)[] ? (null extends Type ? (CheckAttributesField, QueryType> | null) : CheckAttributesField)[] : CheckAttributesField; +declare type DataTypeFromQuery = null extends BaseType ? CheckIsArray, QueryType> | null : CheckIsArray; declare type CheckAttributesField = Q extends { - attributes: infer R; + query: infer R; } ? _DataTypeFromQuery : _DataTypeFromQuery; declare type IsAnyCompareLeftType = { __any: never; @@ -36,25 +42,11 @@ declare type CollectExtraFields = IsAnyCompareLeftType extends Type declare type _CollectExtraFields = keyof (Type) extends never ? null : Values<{ [key in keyof Type]: CollectExtraFields; }>; -declare type SelectString = T extends string ? T : never; -declare type _ValidateDataTypeExtraFileds = SelectString> extends never ? Type : { +declare type _ValidateDataTypeExtraFileds = Values extends never ? Type : { error: { - extraFields: SelectString>; + extraFields: Values; }; }; -declare type ValidateDataTypeExtraFileds = _ValidateDataTypeExtraFileds, Type>; -declare type RequestBase = { - api: string; - query: any; - params?: any; - _meta?: { - data: any; - }; -}; -declare type DataTypeBaseFromRequestType = R extends { - _meta?: { - data: infer DataType; - }; -} ? DataType : never; -export declare type DataTypeFromRequest = ValidateDataTypeExtraFileds, R['query']>>; +declare type ValidateDataTypeExtraFileds = _ValidateDataTypeExtraFileds, null>, Type>; +export declare type DataTypeFromQueryPair = ValidateDataTypeExtraFileds>; export {}; diff --git a/core/hooksBase.d.ts b/core/hooksBase.d.ts index 0d6bf64..feab79b 100644 --- a/core/hooksBase.d.ts +++ b/core/hooksBase.d.ts @@ -5,9 +5,9 @@ interface ModelStatus { } export declare type DataAndStatus = [T | null, ModelStatus]; export interface Request { - api: string; + field: string; params?: any; - query: any; + query?: any; } interface ArSyncModel { data: T | null; diff --git a/core/parseRequest.d.ts b/core/parseRequest.d.ts new file mode 100644 index 0000000..98f4223 --- /dev/null +++ b/core/parseRequest.d.ts @@ -0,0 +1 @@ +export declare function parseRequest(request: any, attrsonly?: any): {}; diff --git a/core/parseRequest.js b/core/parseRequest.js new file mode 100644 index 0000000..421cb9f --- /dev/null +++ b/core/parseRequest.js @@ -0,0 +1,43 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +function parseRequest(request, attrsonly) { + const query = {}; + let field = null; + let params = null; + if (!request) + request = []; + if (request.constructor !== Array) + request = [request]; + for (const arg of request) { + if (typeof (arg) === 'string') { + query[arg] = {}; + } + else if (typeof (arg) === 'object') { + for (const key in arg) { + const value = arg[key]; + if (attrsonly) { + query[key] = parseRequest(value); + continue; + } + if (key === 'query') { + const child = parseRequest(value, true); + for (const k in child) + query[k] = child[k]; + } + else if (key === 'field') { + field = value; + } + else if (key === 'params') { + params = value; + } + else { + query[key] = parseRequest(value); + } + } + } + } + if (attrsonly) + return query; + return { query, field, params }; +} +exports.parseRequest = parseRequest; diff --git a/graph/ArSyncStore.js b/graph/ArSyncStore.js index 35c8a8b..c686afa 100644 --- a/graph/ArSyncStore.js +++ b/graph/ArSyncStore.js @@ -1,26 +1,27 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const ArSyncApi_1 = require("../core/ArSyncApi"); +const parseRequest_1 = require("../core/parseRequest"); const ModelBatchRequest = { timer: null, apiRequests: {}, - fetch(api, query, id) { + fetch(field, query, id) { this.setTimer(); return new Promise(resolve => { const queryJSON = JSON.stringify(query); - const apiRequest = this.apiRequests[api] = this.apiRequests[api] || {}; + const apiRequest = this.apiRequests[field] = this.apiRequests[field] || {}; const queryRequests = apiRequest[queryJSON] = apiRequest[queryJSON] || { query, requests: {} }; const request = queryRequests.requests[id] = queryRequests.requests[id] || { id, callbacks: [] }; request.callbacks.push(resolve); }); }, batchFetch() { - const { apiRequests } = this; - for (const api in apiRequests) { - const apiRequest = apiRequests[api]; + const { apiRequests } = ModelBatchRequest; + for (const field in apiRequests) { + const apiRequest = apiRequests[field]; for (const { query, requests } of Object.values(apiRequest)) { const ids = Object.values(requests).map(({ id }) => id); - ArSyncApi_1.default.syncFetch({ api, query, params: { ids } }).then((models) => { + ArSyncApi_1.default.syncFetch({ field, query, params: { ids } }).then((models) => { for (const model of models) requests[model.id].model = model; for (const { model, callbacks } of Object.values(requests)) { @@ -30,14 +31,14 @@ const ModelBatchRequest = { }); } } - this.apiRequests = {}; + ModelBatchRequest.apiRequests = {}; }, setTimer() { - if (this.timer) + if (ModelBatchRequest.timer) return; - this.timer = setTimeout(() => { - this.timer = null; - this.batchFetch(); + ModelBatchRequest.timer = setTimeout(() => { + ModelBatchRequest.timer = null; + ModelBatchRequest.batchFetch(); }, 20); } }; @@ -87,56 +88,16 @@ class ArSyncContainerBase { l.unsubscribe(); this.listeners = []; } - static parseQuery(query, attrsonly = false) { - const attributes = {}; - let column = null; - let params = null; - if (!query) - query = []; - if (query.constructor !== Array) - query = [query]; - for (const arg of query) { - if (typeof (arg) === 'string') { - attributes[arg] = {}; - } - else if (typeof (arg) === 'object') { - for (const key in arg) { - const value = arg[key]; - if (attrsonly) { - attributes[key] = this.parseQuery(value); - continue; - } - if (key === 'attributes') { - const child = this.parseQuery(value, true); - for (const k in child) - attributes[k] = child[k]; - } - else if (key === 'as') { - column = value; - } - else if (key === 'params') { - params = value; - } - else { - attributes[key] = this.parseQuery(value); - } - } - } - } - if (attrsonly) - return attributes; - return { attributes, as: column, params }; - } - static _load({ api, id, params, query }, root) { - const parsedQuery = ArSyncRecord.parseQuery(query); + static _load({ field, id, params, query }, root) { + const parsedQuery = parseRequest_1.parseRequest(query, true); if (id) { - return ModelBatchRequest.fetch(api, query, id).then(data => new ArSyncRecord(parsedQuery, data[0], null, root)); + return ModelBatchRequest.fetch(field, query, id).then(data => new ArSyncRecord(parsedQuery, data, null, root)); } else { - const request = { api, query, params }; + const request = { field, query, params }; return ArSyncApi_1.default.syncFetch(request).then((response) => { if (response.collection && response.order) { - return new ArSyncCollection(response.sync_keys, 'collection', parsedQuery, response, request, root); + return new ArSyncCollection(response.sync_keys, 'collection', parsedQuery, params, response, request, root); } else { return new ArSyncRecord(parsedQuery, response, request, root); @@ -162,11 +123,11 @@ class ArSyncContainerBase { } } class ArSyncRecord extends ArSyncContainerBase { - constructor(query, data, request, root) { + constructor(query, data, initialRequest, root) { super(); this.root = root; - if (request) - this.initForReload(request); + if (initialRequest) + this.initForReload(initialRequest); this.query = query; this.data = {}; this.children = {}; @@ -186,49 +147,49 @@ class ArSyncRecord extends ArSyncContainerBase { this.data.id = data.id; } this.paths = []; - for (const key in this.query.attributes) { - const subQuery = this.query.attributes[key]; - const aliasName = subQuery.as || key; - const subData = data[aliasName]; + for (const key in this.query) { + const queryField = this.query[key]; + const aliasName = queryField.field || key; + const subData = data[key]; if (key === 'sync_keys') continue; - if (subQuery.attributes && (subData instanceof Array || (subData && subData.collection && subData.order))) { - if (this.children[aliasName]) { - this.children[aliasName].replaceData(subData, this.sync_keys); + if (queryField.query && (subData instanceof Array || (subData && subData.collection && subData.order))) { + if (this.children[key]) { + this.children[key].replaceData(subData, this.sync_keys); } else { - const collection = new ArSyncCollection(this.sync_keys, key, subQuery, subData, null, this.root); + const collection = new ArSyncCollection(this.sync_keys, aliasName, queryField.query, queryField.params, subData, null, this.root); this.mark(); - this.children[aliasName] = collection; - this.data[aliasName] = collection.data; + this.children[key] = collection; + this.data[key] = collection.data; collection.parentModel = this; - collection.parentKey = aliasName; + collection.parentKey = key; } } else { - if (subQuery.attributes && Object.keys(subQuery.attributes).length > 0) + if (queryField.query && Object.keys(queryField.query).length > 0) this.paths.push(key); if (subData && subData.sync_keys) { - if (this.children[aliasName]) { - this.children[aliasName].replaceData(subData); + if (this.children[key]) { + this.children[key].replaceData(subData); } else { - const model = new ArSyncRecord(subQuery, subData, null, this.root); + const model = new ArSyncRecord(queryField.query, subData, null, this.root); this.mark(); - this.children[aliasName] = model; - this.data[aliasName] = model.data; + this.children[key] = model; + this.data[key] = model.data; model.parentModel = this; - model.parentKey = aliasName; + model.parentKey = key; } } else { - if (this.children[aliasName]) { - this.children[aliasName].release(); - delete this.children[aliasName]; + if (this.children[key]) { + this.children[key].release(); + delete this.children[key]; } - if (this.data[aliasName] !== subData) { + if (this.data[key] !== subData) { this.mark(); - this.data[aliasName] = subData; + this.data[key] = subData; } } } @@ -255,7 +216,7 @@ class ArSyncRecord extends ArSyncContainerBase { else if (action === 'add') { if (this.data.id === id) return; - const query = this.query.attributes[path]; + const query = this.query[path].query; ModelBatchRequest.fetch(class_name, query, id).then(data => { if (!data) return; @@ -290,16 +251,16 @@ class ArSyncRecord extends ArSyncContainerBase { reloadQuery() { if (this.reloadQueryCache) return this.reloadQueryCache; - const reloadQuery = this.reloadQueryCache = { attributes: [] }; - for (const key in this.query.attributes) { + const reloadQuery = this.reloadQueryCache = { query: [] }; + for (const key in this.query) { if (key === 'sync_keys') continue; - const val = this.query.attributes[key]; - if (!val || !val.attributes) { - reloadQuery.attributes.push(key); + const val = this.query[key]; + if (!val || !val.query) { + reloadQuery.query.push(key); } - else if (!val.params && Object.keys(val.attributes).length === 0) { - reloadQuery.attributes.push({ [key]: val }); + else if (!val.params && Object.keys(val.query).length === 0) { + reloadQuery.query.push({ [key]: val }); } } return reloadQuery; @@ -334,14 +295,14 @@ class ArSyncRecord extends ArSyncContainerBase { } } class ArSyncCollection extends ArSyncContainerBase { - constructor(sync_keys, path, query, data, request, root) { + constructor(sync_keys, path, query, params, data, initialRequest, root) { super(); this.root = root; this.path = path; - if (request) - this.initForReload(request); - if (query.params && (query.params.order || query.params.limit)) { - this.order = { limit: query.params.limit, mode: query.params.order || 'asc' }; + if (initialRequest) + this.initForReload(initialRequest); + if (params && (params.order || params.limit)) { + this.order = { limit: params.limit, mode: params.order || 'asc' }; } else { this.order = { limit: null, mode: 'asc' }; diff --git a/lib/ar_sync/core.rb b/lib/ar_sync/core.rb index 008dfbc..b294949 100644 --- a/lib/ar_sync/core.rb +++ b/lib/ar_sync/core.rb @@ -119,13 +119,13 @@ def self.sync_api(model, current_user, args) def self._extract_paths(args) parsed = ArSerializer::Serializer.parse_args args paths = [] - extract = lambda do |path, attributes| + extract = lambda do |path, query| paths << path - attributes.each do |key, value| - sub_attributes = value[:attributes] - next unless sub_attributes - sub_path = [*path, key] - extract.call sub_path, sub_attributes + query&.each do |key, value| + sub_query = value[:attributes] + next unless sub_query + sub_path = [*path, (value[:field_name] || key)] + extract.call sub_path, sub_query end end extract.call [], parsed[:attributes] diff --git a/lib/ar_sync/rails.rb b/lib/ar_sync/rails.rb index 0ade33b..9af2d2c 100644 --- a/lib/ar_sync/rails.rb +++ b/lib/ar_sync/rails.rb @@ -99,7 +99,7 @@ def _api_call(type) end responses = params[:requests].map do |request| begin - api_name = request[:api] + api_name = request[:field] info = self.class._serializer_field_info api_name raise ArSync::ApiNotFound, "#{type.to_s.capitalize} API named `#{api_name}` not configured" unless info api_params = (request[:params].as_json || {}).transform_keys(&:to_sym) diff --git a/lib/ar_sync/type_script.rb b/lib/ar_sync/type_script.rb index 8464c35..cc2ccb5 100644 --- a/lib/ar_sync/type_script.rb +++ b/lib/ar_sync/type_script.rb @@ -11,69 +11,46 @@ def self.generate_typed_files(api_class, dir:, mode: nil, comment: nil) end def self.generate_type_definition(api_class) + schema = Class.new + schema.define_singleton_method(:name) { 'Schema' } + schema.define_singleton_method(:ancestors) { api_class.ancestors } + schema.define_singleton_method(:method_missing) { |*args| api_class.send(*args) } + classes = [schema] + api_related_classes(api_class) - [api_class] [ - ArSerializer::TypeScript.generate_type_definition(api_related_classes(api_class)), - request_type_definition(api_class) + "import { DataTypeFromQueryPair } from 'ar_sync/core/DataType'", + ArSerializer::TypeScript.generate_type_definition(classes), + <<~CODE + type ExtractData = T extends { data: infer D } ? D : T + export type DataTypeFromRootQuery = + ExtractData> + export type TypeRootQuery = TypeSchemaAliasFieldQuery + CODE ].join "\n" end def self.api_related_classes(api_class) - classes = ArSerializer::TypeScript.related_serializer_types([api_class]).map(&:type) - classes - [api_class] - end - - def self.request_type_definition(api_class) - type = ArSerializer::GraphQL::TypeClass.from api_class - definitions = [] - request_types = {} - type.fields.each do |field| - association_type = field.type.association_type - next unless association_type - prefix = 'Class' if field.name.match?(/\A[A-Z]/) # for class reload query - request_type_name = "Type#{prefix}#{field.name.camelize}Request" - request_types[field.name] = request_type_name - multiple = field.type.is_a? ArSerializer::GraphQL::ListTypeClass - definitions << <<~CODE - export interface #{request_type_name} { - api: '#{field.name}' - params?: #{field.args_ts_type} - query: Type#{association_type.name}Query - _meta?: { data: Type#{field.type.association_type.name}#{'[]' if multiple} } - } - CODE - end - [ - 'export type TypeRequest = ', - request_types.values.map { |value| " | #{value}" }, - 'export type ApiNameRequests = {', - request_types.map { |key, value| " #{key}: #{value}" }, - '}', - definitions - ].join("\n") + ArSerializer::TypeScript.related_serializer_types([api_class]).map(&:type) end def self.generate_model_script(mode) <<~CODE - import { TypeRequest, ApiNameRequests } from './types' - import { DataTypeFromRequest } from 'ar_sync/core/DataType' + import { TypeSchema, TypeRootQuery, DataTypeFromRootQuery } from './types' import ArSyncModelBase from 'ar_sync/#{mode}/ArSyncModel' - export default class ArSyncModel extends ArSyncModelBase<{}> { - constructor(r: R) { super(r) } - data: DataTypeFromRequest | null + export default class ArSyncModel extends ArSyncModelBase> { + constructor(q: Q) { super(q) } } CODE end def self.generate_hooks_script(mode) <<~CODE - import { TypeRequest, ApiNameRequests } from './types' - import { DataTypeFromRequest } from 'ar_sync/core/DataType' + import { TypeSchema, TypeRootQuery, DataTypeFromRootQuery } from './types' import { useArSyncModel as useArSyncModelBase, useArSyncFetch as useArSyncFetchBase } from 'ar_sync/#{mode}/hooks' - export function useArSyncModel(request: R | null) { - return useArSyncModelBase>(request) + export function useArSyncModel(request: Q | null) { + return useArSyncModelBase>(request) } - export function useArSyncFetch(request: R | null) { - return useArSyncFetchBase>(request) + export function useArSyncFetch(request: Q | null) { + return useArSyncFetchBase>(request) } CODE end diff --git a/package.json b/package.json index 000f454..b3cfa0a 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "actioncable": "^5.2.0", "@types/actioncable": "^5.2.0", "eslint": "^5.16.0", - "typescript": "^3.4.5", + "typescript": "^3.5.1", "@typescript-eslint/eslint-plugin": "^1.6.0", "@typescript-eslint/parser": "^1.6.0" } diff --git a/src/core/ArSyncModelBase.ts b/src/core/ArSyncModelBase.ts index fdafcb9..f5b23aa 100644 --- a/src/core/ArSyncModelBase.ts +++ b/src/core/ArSyncModelBase.ts @@ -1,4 +1,4 @@ -interface Request { api: string; query: any; params?: any } +interface Request { field: string; params?: any; query?: any } type Path = (string | number)[] interface Change { path: Path; value: any } type ChangeCallback = (change: Change) => void diff --git a/src/core/DataType.ts b/src/core/DataType.ts index 6570e3f..5d87cd3 100644 --- a/src/core/DataType.ts +++ b/src/core/DataType.ts @@ -16,37 +16,54 @@ interface ExtraFieldErrorType { type DataTypeExtractFromQueryHash = '*' extends keyof QueryType ? { - [key in Exclude<(keyof BaseType) | (keyof QueryType), '_meta' | '_params' | '*'>]: (key extends keyof BaseType - ? (key extends keyof QueryType - ? (QueryType[key] extends true - ? DataTypeExtractField - : DataTypeFromQuery) - : DataTypeExtractField) - : ExtraFieldErrorType) - } - : { - [key in keyof QueryType]: (key extends keyof BaseType - ? (QueryType[key] extends true - ? DataTypeExtractField - : DataTypeFromQuery) - : ExtraFieldErrorType) + [key in Exclude<(keyof BaseType) | (keyof QueryType), '_meta' | '*'>]: ( + key extends keyof QueryType + ? _DataTypePickField + : key extends keyof BaseType + ? DataTypeExtractField + : ExtraFieldErrorType + ) } + : { [key in keyof QueryType]: _DataTypePickField } + +type _DataTypePickField = + SubQuery extends { field: infer N, query?: infer Q } + ? ( + N extends keyof BaseType + ? ( + IsAnyCompareLeftType extends Q + ? DataTypeExtractField + : DataTypeFromQuery) + : ExtraFieldErrorType + ) + : ( + Key extends keyof BaseType + ? (SubQuery extends true | { query?: true | never; params: any } + ? DataTypeExtractField + : DataTypeFromQuery) + : ExtraFieldErrorType + ) type _DataTypeFromQuery = QueryType extends keyof BaseType | '*' ? DataTypeExtractFieldsFromQuery : QueryType extends Readonly<(keyof BaseType | '*')[]> ? DataTypeExtractFieldsFromQuery> - : QueryType extends { as: string } - ? { error: 'type for alias field is not supported' } | undefined : DataTypeExtractFromQueryHash -export type DataTypeFromQuery = BaseType extends any[] - ? CheckAttributesField[] - : null extends BaseType - ? CheckAttributesField | null - : CheckAttributesField +type CheckIsArray = BaseType extends (infer Type)[] + ? ( + null extends Type + ? (CheckAttributesField, QueryType> | null) + : CheckAttributesField + )[] + : CheckAttributesField -type CheckAttributesField = Q extends { attributes: infer R } +type DataTypeFromQuery = + null extends BaseType + ? CheckIsArray, QueryType> | null + : CheckIsArray + +type CheckAttributesField = Q extends { query: infer R } ? _DataTypeFromQuery : _DataTypeFromQuery @@ -64,14 +81,10 @@ type _CollectExtraFields = keyof (Type) extends never ? null : Values<{ [key in keyof Type]: CollectExtraFields }> -type SelectString = T extends string ? T : never -type _ValidateDataTypeExtraFileds = SelectString> extends never +type _ValidateDataTypeExtraFileds = Values extends never ? Type - : { error: { extraFields: SelectString> } } -type ValidateDataTypeExtraFileds = _ValidateDataTypeExtraFileds, Type> + : { error: { extraFields: Values } } + +type ValidateDataTypeExtraFileds = _ValidateDataTypeExtraFileds, null>, Type> -type RequestBase = { api: string; query: any; params?: any; _meta?: { data: any } } -type DataTypeBaseFromRequestType = R extends { _meta?: { data: infer DataType } } ? DataType : never -export type DataTypeFromRequest = ValidateDataTypeExtraFileds< - DataTypeFromQuery, R['query']> -> +export type DataTypeFromQueryPair = ValidateDataTypeExtraFileds> diff --git a/src/core/hooksBase.ts b/src/core/hooksBase.ts index 8e61232..39cffdf 100644 --- a/src/core/hooksBase.ts +++ b/src/core/hooksBase.ts @@ -3,7 +3,7 @@ import ArSyncAPI from './ArSyncApi' interface ModelStatus { complete: boolean; notfound?: boolean; connected: boolean } export type DataAndStatus = [T | null, ModelStatus] -export interface Request { api: string; params?: any; query: any } +export interface Request { field: string; params?: any; query?: any } interface ArSyncModel { data: T | null diff --git a/src/core/parseRequest.ts b/src/core/parseRequest.ts new file mode 100644 index 0000000..654ff1d --- /dev/null +++ b/src/core/parseRequest.ts @@ -0,0 +1,32 @@ +export function parseRequest(request, attrsonly?){ + const query = {} + let field = null + let params = null + if (!request) request = [] + if (request.constructor !== Array) request = [request] + for (const arg of request) { + if (typeof(arg) === 'string') { + query[arg] = {} + } else if (typeof(arg) === 'object') { + for (const key in arg){ + const value = arg[key] + if (attrsonly) { + query[key] = parseRequest(value) + continue + } + if (key === 'query') { + const child = parseRequest(value, true) + for (const k in child) query[k] = child[k] + } else if (key === 'field') { + field = value + } else if (key === 'params') { + params = value + } else { + query[key] = parseRequest(value) + } + } + } + } + if (attrsonly) return query + return { query, field, params } +} diff --git a/src/graph/ArSyncStore.ts b/src/graph/ArSyncStore.ts index 091af14..48911b3 100644 --- a/src/graph/ArSyncStore.ts +++ b/src/graph/ArSyncStore.ts @@ -1,9 +1,10 @@ import ArSyncAPI from '../core/ArSyncApi' +import { parseRequest } from '../core/parseRequest' const ModelBatchRequest = { - timer: null, + timer: null as null | number, apiRequests: {} as { - [api: string]: { + [key: string]: { [queryJSON: string]: { query requests: { @@ -16,23 +17,23 @@ const ModelBatchRequest = { } } }, - fetch(api, query, id) { + fetch(field, query, id) { this.setTimer() return new Promise(resolve => { const queryJSON = JSON.stringify(query) - const apiRequest = this.apiRequests[api] = this.apiRequests[api] || {} + const apiRequest = this.apiRequests[field] = this.apiRequests[field] || {} const queryRequests = apiRequest[queryJSON] = apiRequest[queryJSON] || { query, requests: {} } const request = queryRequests.requests[id] = queryRequests.requests[id] || { id, callbacks: [] } request.callbacks.push(resolve) }) }, batchFetch() { - const { apiRequests } = this as typeof ModelBatchRequest - for (const api in apiRequests) { - const apiRequest = apiRequests[api] + const { apiRequests } = ModelBatchRequest + for (const field in apiRequests) { + const apiRequest = apiRequests[field] for (const { query, requests } of Object.values(apiRequest)) { const ids = Object.values(requests).map(({ id }) => id) - ArSyncAPI.syncFetch({ api, query, params: { ids } }).then((models: any[]) => { + ArSyncAPI.syncFetch({ field, query, params: { ids } }).then((models: any[]) => { for (const model of models) requests[model.id].model = model for (const { model, callbacks } of Object.values(requests)) { for (const callback of callbacks) callback(model) @@ -40,13 +41,13 @@ const ModelBatchRequest = { }) } } - this.apiRequests = {} + ModelBatchRequest.apiRequests = {} }, setTimer() { - if (this.timer) return - this.timer = setTimeout(() => { - this.timer = null - this.batchFetch() + if (ModelBatchRequest.timer) return + ModelBatchRequest.timer = setTimeout(() => { + ModelBatchRequest.timer = null + ModelBatchRequest.batchFetch() }, 20) } } @@ -96,47 +97,15 @@ class ArSyncContainerBase { for (const l of this.listeners) l.unsubscribe() this.listeners = [] } - static parseQuery(query, attrsonly = false){ - const attributes = {} - let column = null - let params = null - if (!query) query = [] - if (query.constructor !== Array) query = [query] - for (const arg of query) { - if (typeof(arg) === 'string') { - attributes[arg] = {} - } else if (typeof(arg) === 'object') { - for (const key in arg){ - const value = arg[key] - if (attrsonly) { - attributes[key] = this.parseQuery(value) - continue - } - if (key === 'attributes') { - const child = this.parseQuery(value, true) - for (const k in child) attributes[k] = child[k] - } else if (key === 'as') { - column = value - } else if (key === 'params') { - params = value - } else { - attributes[key] = this.parseQuery(value) - } - } - } - } - if (attrsonly) return attributes - return { attributes, as: column, params } - } - static _load({ api, id, params, query }, root) { - const parsedQuery = ArSyncRecord.parseQuery(query) + static _load({ field, id, params, query }, root) { + const parsedQuery = parseRequest(query, true) if (id) { - return ModelBatchRequest.fetch(api, query, id).then(data => new ArSyncRecord(parsedQuery, data[0], null, root)) + return ModelBatchRequest.fetch(field, query, id).then(data => new ArSyncRecord(parsedQuery, data, null, root)) } else { - const request = { api, query, params } + const request = { field, query, params } return ArSyncAPI.syncFetch(request).then((response: any) => { if (response.collection && response.order) { - return new ArSyncCollection(response.sync_keys, 'collection', parsedQuery, response, request, root) + return new ArSyncCollection(response.sync_keys, 'collection', parsedQuery, params, response, request, root) } else { return new ArSyncRecord(parsedQuery, response, request, root) } @@ -168,10 +137,10 @@ class ArSyncRecord extends ArSyncContainerBase { sync_keys paths reloadQueryCache - constructor(query, data, request, root) { + constructor(query, data, initialRequest, root) { super() this.root = root - if (request) this.initForReload(request) + if (initialRequest) this.initForReload(initialRequest) this.query = query this.data = {} this.children = {} @@ -191,43 +160,43 @@ class ArSyncRecord extends ArSyncContainerBase { this.data.id = data.id } this.paths = [] - for (const key in this.query.attributes) { - const subQuery = this.query.attributes[key] - const aliasName = subQuery.as || key - const subData = data[aliasName] + for (const key in this.query) { + const queryField = this.query[key] + const aliasName = queryField.field || key + const subData = data[key] if (key === 'sync_keys') continue - if (subQuery.attributes && (subData instanceof Array || (subData && subData.collection && subData.order))) { - if (this.children[aliasName]) { - this.children[aliasName].replaceData(subData, this.sync_keys) + if (queryField.query && (subData instanceof Array || (subData && subData.collection && subData.order))) { + if (this.children[key]) { + this.children[key].replaceData(subData, this.sync_keys) } else { - const collection = new ArSyncCollection(this.sync_keys, key, subQuery, subData, null, this.root) + const collection = new ArSyncCollection(this.sync_keys, aliasName, queryField.query, queryField.params, subData, null, this.root) this.mark() - this.children[aliasName] = collection - this.data[aliasName] = collection.data + this.children[key] = collection + this.data[key] = collection.data collection.parentModel = this - collection.parentKey = aliasName + collection.parentKey = key } } else { - if (subQuery.attributes && Object.keys(subQuery.attributes).length > 0) this.paths.push(key); + if (queryField.query && Object.keys(queryField.query).length > 0) this.paths.push(key); if (subData && subData.sync_keys) { - if (this.children[aliasName]) { - this.children[aliasName].replaceData(subData) + if (this.children[key]) { + this.children[key].replaceData(subData) } else { - const model = new ArSyncRecord(subQuery, subData, null, this.root) + const model = new ArSyncRecord(queryField.query, subData, null, this.root) this.mark() - this.children[aliasName] = model - this.data[aliasName] = model.data + this.children[key] = model + this.data[key] = model.data model.parentModel = this - model.parentKey = aliasName + model.parentKey = key } } else { - if(this.children[aliasName]) { - this.children[aliasName].release() - delete this.children[aliasName] + if(this.children[key]) { + this.children[key].release() + delete this.children[key] } - if (this.data[aliasName] !== subData) { + if (this.data[key] !== subData) { this.mark() - this.data[aliasName] = subData + this.data[key] = subData } } } @@ -252,7 +221,7 @@ class ArSyncRecord extends ArSyncContainerBase { this.onChange([path], null) } else if (action === 'add') { if (this.data.id === id) return - const query = this.query.attributes[path] + const query = this.query[path].query ModelBatchRequest.fetch(class_name, query, id).then(data => { if (!data) return const model = new ArSyncRecord(query, data, null, this.root) @@ -282,14 +251,14 @@ class ArSyncRecord extends ArSyncContainerBase { } reloadQuery() { if (this.reloadQueryCache) return this.reloadQueryCache - const reloadQuery = this.reloadQueryCache = { attributes: [] as any[] } - for (const key in this.query.attributes) { + const reloadQuery = this.reloadQueryCache = { query: [] as any[] } + for (const key in this.query) { if (key === 'sync_keys') continue - const val = this.query.attributes[key] - if (!val || !val.attributes) { - reloadQuery.attributes.push(key) - } else if (!val.params && Object.keys(val.attributes).length === 0) { - reloadQuery.attributes.push({ [key]: val }) + const val = this.query[key] + if (!val || !val.query) { + reloadQuery.query.push(key) + } else if (!val.params && Object.keys(val.query).length === 0) { + reloadQuery.query.push({ [key]: val }) } } return reloadQuery @@ -326,13 +295,13 @@ class ArSyncCollection extends ArSyncContainerBase { data children sync_keys - constructor(sync_keys, path, query, data, request, root){ + constructor(sync_keys, path, query, params, data, initialRequest, root){ super() this.root = root this.path = path - if (request) this.initForReload(request) - if (query.params && (query.params.order || query.params.limit)) { - this.order = { limit: query.params.limit, mode: query.params.order || 'asc' } + if (initialRequest) this.initForReload(initialRequest) + if (params && (params.order || params.limit)) { + this.order = { limit: params.limit, mode: params.order || 'asc' } } else { this.order = { limit: null, mode: 'asc' } } diff --git a/src/tree/ArSyncStore.ts b/src/tree/ArSyncStore.ts index 040248c..4a82e31 100644 --- a/src/tree/ArSyncStore.ts +++ b/src/tree/ArSyncStore.ts @@ -1,3 +1,4 @@ +import { parseRequest } from '../core/parseRequest' class Updator { changes markedForFreezeObjects @@ -167,11 +168,11 @@ class Updator { export default class ArSyncStore { data - query + request immutable - constructor(query, data, option = {} as { immutable?: boolean }) { + constructor(request, data, option = {} as { immutable?: boolean }) { this.data = option.immutable ? Updator.createFrozenObject(data) : data - this.query = ArSyncStore.parseQuery(query) + this.request = parseRequest(request) this.immutable = option.immutable } replaceData(data) { @@ -188,136 +189,110 @@ export default class ArSyncStore { return this.batchUpdate([patch]) } _slicePatch(patchData, query) { - const obj = {} - for (const key in patchData) { - if (key === 'id' || query.attributes['*']) { - obj[key] = patchData[key] - } else { - const subq = query.attributes[key] - if (subq) { - obj[subq.column || key] = patchData[key] - } - } + const obj = query && query['*'] ? { ...patchData } : {} + for (const key in query) { + const fieldQuery = query[key] + const field = (fieldQuery && fieldQuery.field) || key + if (field in patchData) obj[key] = patchData[field] } + if (patchData.id) obj.id = patchData.id return obj } _applyPatch(data, accessKeys, actualPath, updator, query, patchData) { for (const key in patchData) { - const subq = query.attributes[key] - const value = patchData[key] - if (subq || query.attributes['*']) { - const subcol = (subq && subq.column) || key - if (data[subcol] !== value) { - this.data = updator.add(this.data, accessKeys, actualPath, subcol, value) + const subq = query[key] + const value = patchData[(subq && subq.field) || key] + if (subq || query['*']) { + if (data[key] !== value) { + this.data = updator.add(this.data, accessKeys, actualPath, key, value) } } } } - _update(patch, updator, events) { + _update(patch: { action: string; path: (string | number)[]; ordering; data }, updator, events) { const { action, path } = patch const patchData = patch.data - let query = this.query + let request = this.request let data = this.data - const actualPath: (string | number)[] = [] - const accessKeys: (string | number)[] = [] - for (let i = 0; i < path.length - 1; i++) { + const trace = (i: number, actualPath: (string | number)[], accessKeys: (string | number)[], query, data) => { const nameOrId = path[i] + const lastStep = i === path.length - 1 if (typeof(nameOrId) === 'number') { const idx = data.findIndex(o => o.id === nameOrId) - if (idx < 0) return - actualPath.push(nameOrId) - accessKeys.push(idx) - data = data[idx] + if (lastStep) { + apply(accessKeys, actualPath, query, null, idx, data[idx]) + } else { + if (idx < 0) return + actualPath.push(nameOrId) + accessKeys.push(idx) + const data2 = data[idx] + trace(i + 1, actualPath, accessKeys, query, data2) + } } else { - const { attributes } = query - if (!attributes[nameOrId]) return - const column = attributes[nameOrId].column || nameOrId - query = attributes[nameOrId] - actualPath.push(column) - accessKeys.push(column) - data = data[column] + const matchedKeys: string[] = [] + for (const key in query) { + const field = query[key].field || key + if (field === nameOrId) matchedKeys.push(key) + } + const fork = matchedKeys.length > 1 + for (const key of matchedKeys) { + const queryField = query[key] + if (lastStep) { + if (!queryField) return + apply(accessKeys, actualPath, queryField.query, key, null, data[key]) + } else { + const data2 = data[key] + if (!data2) return + const actualPath2 = fork ? [...actualPath] : actualPath + const accessKeys2 = fork ? [...accessKeys] : accessKeys + actualPath2.push(key) + accessKeys2.push(key) + trace(i + 1, actualPath2, accessKeys2, queryField.query, data2) + } + } } - if (!data) return } - const nameOrId = path[path.length - 1] - let id, idx, column, target = data - if (typeof(nameOrId) === 'number') { - id = nameOrId - idx = data.findIndex(o => o.id === id) - target = data[idx] - } else if (nameOrId) { - const { attributes } = query - if (!attributes[nameOrId]) return - column = attributes[nameOrId].column || nameOrId - query = attributes[nameOrId] - target = data[column] - } - if (action === 'create') { - const obj = this._slicePatch(patchData, query) - if (column) { - this.data = updator.add(this.data, accessKeys, actualPath, column, obj) - } else if (!target) { - const ordering = Object.assign({}, patch.ordering) - const limitOverride = query.params && query.params.limit - ordering.order = query.params && query.params.order || ordering.order - if (ordering.limit == null || limitOverride != null && limitOverride < ordering.limit) ordering.limit = limitOverride - this.data = updator.add(this.data, accessKeys, actualPath, data.length, obj, ordering) + const apply = (accessKeys: (string | number)[], actualPath: (string | number)[], query, column: string | null, idx: number | null, target) => { + if (action === 'create') { + const obj = this._slicePatch(patchData, query) + if (column) { + this.data = updator.add(this.data, accessKeys, actualPath, column, obj) + } else if (!target) { + const ordering = Object.assign({}, patch.ordering) + const limitOverride = request.params && request.params.limit + ordering.order = request.params && request.params.order || ordering.order + if (ordering.limit == null || limitOverride != null && limitOverride < ordering.limit) ordering.limit = limitOverride + this.data = updator.add(this.data, accessKeys, actualPath, data.length, obj, ordering) + } + return } - return - } - if (action === 'destroy') { + if (action === 'destroy') { + if (column) { + this.data = updator.remove(this.data, accessKeys, actualPath, column) + } else if (idx != null) { + this.data = updator.remove(this.data, accessKeys, actualPath, idx) + } + return + } + if (!target) return if (column) { - this.data = updator.remove(this.data, accessKeys, actualPath, column) - } else if (idx >= 0) { - this.data = updator.remove(this.data, accessKeys, actualPath, idx) + actualPath.push(column) + accessKeys.push(column) + } else if (idx != null && patchData.id) { + actualPath.push(patchData.id) + accessKeys.push(idx) + } + if (action === 'update') { + this._applyPatch(target, accessKeys, actualPath, updator, query, patchData) + } else { + const eventData = { target, path: actualPath, data: patchData.data } + events.push({ type: patchData.type, data: eventData }) } - return - } - if (!target) return - if (column) { - actualPath.push(column) - accessKeys.push(column) - } else if (id) { - actualPath.push(id) - accessKeys.push(idx) } - if (action === 'update') { - this._applyPatch(target, accessKeys, actualPath, updator, query, patchData) + if (path.length === 0) { + apply([], [], request.query, null, null, data) } else { - const eventData = { target, path: actualPath, data: patchData.data } - events.push({ type: patchData.type, data: eventData }) - } - } - - static parseQuery(query, attrsonly?){ - const attributes = {} - let column = null - let params = null - if (query.constructor !== Array) query = [query] - for (const arg of query) { - if (typeof(arg) === 'string') { - attributes[arg] = {} - } else if (typeof(arg) === 'object') { - for (const key in arg){ - const value = arg[key] - if (attrsonly) { - attributes[key] = this.parseQuery(value) - continue - } - if (key === 'attributes') { - const child = this.parseQuery(value, true) - for (const k in child) attributes[k] = child[k] - } else if (key === 'as') { - column = value - } else if (key === 'params') { - params = value - } else { - attributes[key] = this.parseQuery(value) - } - } - } + trace(0, [], [], request.query, data) } - if (attrsonly) return attributes - return { attributes, column, params } } } diff --git a/test/ar_sync_test.rb b/test/ar_sync_test.rb index b024fad..03fe0a1 100644 --- a/test/ar_sync_test.rb +++ b/test/ar_sync_test.rb @@ -9,9 +9,29 @@ end end -query = [name: { as: '名前' }, posts: [:user, :title, as: :articles, my_comments: [:star_count, as: :my_opinions], comments: [:star_count, :user, my_stars: :id, my_star: { as: :my_reaction }]]] -post_query = [:user, :title, comments: [:body, as: :cmnts]] -collection_query = [:user, :title, my_comments: [:star_count, as: :my_opinions], comments: [:star_count, :user, my_stars: :id, my_star: { as: :my_reaction }]] +query = { + 名前: { field: :name }, + articles: { + field: :posts, + query: { + user: true, title: true, + my_opinions: { field: :my_comments, query: :star_count }, + comments: { + star_count: true, user: true, my_stars: :id, + my_reaction: { field: :my_star } + } + } + } +} +post_query = { user: true, title: true, cmnts: { field: :comments, query: :body } } +collection_query = { + user: true, title: true, + my_opinions: { field: :my_comments, query: :star_count }, + comments: { + star_count: true, user: true, my_stars: :id, + my_reaction: { field: :my_star } + } +} $test_cases = { user: [Tree::User.first, query], diff --git a/test/ts_test.rb b/test/ts_test.rb index 2145b30..c729c9d 100644 --- a/test/ts_test.rb +++ b/test/ts_test.rb @@ -21,12 +21,12 @@ def test_typed_files dir = 'test/generated_typed_files' Dir.mkdir dif unless Dir.exist? dir ArSync::TypeScript.generate_typed_files Schema, mode: :tree, dir: dir - ['hooks.ts', 'ArSyncModel.ts'].each do |file| + ['hooks.ts', 'ArSyncModel.ts', 'types.ts'].each do |file| path = File.join dir, file File.write path, File.read(path).gsub('ar_sync/', '../../src/') end output = `./node_modules/typescript/bin/tsc --strict --lib es2017 --noEmit test/type_test.ts` - output = output.lines.grep(/type_test/) + output = output.lines.grep(/test/) puts output assert output.empty? end diff --git a/test/tsconfig.json b/test/tsconfig.json new file mode 100644 index 0000000..8957991 --- /dev/null +++ b/test/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es2017", + "strictNullChecks": true, + "noUnusedLocals": true, + "noUnusedParameters": true + }, + "include": ["*"] +} diff --git a/test/type_test.ts b/test/type_test.ts index 3e88118..120ae0a 100644 --- a/test/type_test.ts +++ b/test/type_test.ts @@ -1,43 +1,58 @@ import ArSyncModel from './generated_typed_files/ArSyncModel' import { useArSyncModel, useArSyncFetch } from './generated_typed_files/hooks' -const [hooksData1] = useArSyncModel({ api: 'currentUser', query: 'id' }) -hooksData1!.id -const [hooksData2] = useArSyncModel({ api: 'currentUser', query: { '*': true, foo: true } }) -hooksData2!.error.extraFields = 'foo' -const [hooksData3] = useArSyncFetch({ api: 'currentUser', query: 'id' }) -hooksData3!.id -const [hooksData4] = useArSyncFetch({ api: 'currentUser', query: { '*': true, foo: true } }) -hooksData4!.error.extraFields = 'foo' +type IsEqual = [T, U] extends [U, T] ? true : false +function isOK(): T | undefined { return } +type IsStrictMode = string | null extends string ? false : true +isOK() -const data1 = new ArSyncModel({ api: 'currentUser', query: 'id' }).data! -data1.id -const data2 = new ArSyncModel({ api: 'currentUser', query: ['id', 'name'] }).data! -data2.id; data2.name -const data3 = new ArSyncModel({ api: 'currentUser', query: '*' }).data! -data3.id; data3.name; data3.posts -const data4 = new ArSyncModel({ api: 'currentUser', query: { posts: 'id' } }).data! -data4.posts[0].id -const data5 = new ArSyncModel({ api: 'currentUser', query: { posts: '*' } }).data! -data5.posts[0].id; data5.posts[0].user; data5.posts[0].body -const data6 = new ArSyncModel({ api: 'currentUser', query: { posts: { '*': true, comments: 'user' } } }).data! -data6.posts[0].id; data6.posts[0].user; data6.posts[0].comments[0].user -const data7 = new ArSyncModel({ api: 'currentUser', query: { name: true, poosts: true } }).data! -data7.error.extraFields = 'poosts' -const data8 = new ArSyncModel({ api: 'currentUser', query: { posts: { id: true, commmments: true, titllle: true } } }).data! -data8.error.extraFields = 'commmments' -data8.error.extraFields = 'titllle' -const data9 = new ArSyncModel({ api: 'currentUser', query: { '*': true, posts: { id: true, commmments: true } } }).data! -data9.error.extraFields = 'commmments' -const data10 = new ArSyncModel({ api: 'users', query: { '*': true, posts: { id: true, comments: '*' } } }).data! -data10[0].posts[0].comments[0].id -const data11 = new ArSyncModel({ api: 'users', query: { '*': true, posts: { id: true, comments: '*', commmments: true } } }).data! -data11.error.extraFields = 'commmments' -const data12 = new ArSyncModel({ api: 'currentUser', query: { posts: { params: { limit: 4 }, attributes: 'title' } } }).data! -data12.posts[0].title -const data13 = new ArSyncModel({ api: 'currentUser', query: { posts: { params: { limit: 4 }, attributes: ['id', 'title'] } } }).data! -data13.posts[0].title -const data14 = new ArSyncModel({ api: 'currentUser', query: { posts: { params: { limit: 4 }, attributes: { id: true, title: true } } } }).data! -data14.posts[0].title -const data15 = new ArSyncModel({ api: 'currentUser', query: { posts: ['id', 'title'] } } as const).data! -data15.posts[0].title +const [hooksData1] = useArSyncModel({ field: 'currentUser', query: 'id' }) +isOK>() +const [hooksData2] = useArSyncModel({ field: 'currentUser', query: { '*': true, foo: true } }) +isOK>() +const [hooksData3] = useArSyncFetch({ field: 'currentUser', query: 'id' }) +isOK>() +const [hooksData4] = useArSyncFetch({ field: 'currentUser', query: { '*': true, foo: true } }) +isOK>() + +const data1 = new ArSyncModel({ field: 'currentUser', query: 'id' }).data! +isOK>() +const data2 = new ArSyncModel({ field: 'currentUser', query: ['id', 'name'] }).data! +isOK>() +const data3 = new ArSyncModel({ field: 'currentUser', query: '*' }).data! +isOK>() +const data4 = new ArSyncModel({ field: 'currentUser', query: { posts: 'id' } }).data! +isOK>() +const data5 = new ArSyncModel({ field: 'currentUser', query: { posts: '*' } }).data! +isOK>() +const data6 = new ArSyncModel({ field: 'currentUser', query: { posts: { '*': true, comments: 'user' } } }).data!.posts[0].comments[0] +isOK>() +const data7 = new ArSyncModel({ field: 'currentUser', query: { name: true, poosts: true } }).data! +isOK>() +const data8 = new ArSyncModel({ field: 'currentUser', query: { posts: { id: true, commmments: true, titllle: true } } }).data! +isOK>() +const data9 = new ArSyncModel({ field: 'currentUser', query: { '*': true, posts: { id: true, commmments: true } } }).data! +isOK>() +const data10 = new ArSyncModel({ field: 'users', query: { '*': true, posts: { id: true, comments: '*' } } }).data![0].posts[0].comments[0].id +isOK>() +const data11 = new ArSyncModel({ field: 'users', query: { '*': true, posts: { id: true, comments: '*', commmments: true } } }).data! +isOK>() +const data12 = new ArSyncModel({ field: 'currentUser', query: { posts: { params: { limit: 4 }, query: 'title' } } }).data!.posts[0] +isOK>() +const data13 = new ArSyncModel({ field: 'currentUser', query: { posts: { params: { limit: 4 }, query: ['id', 'title'] } } }).data!.posts[0] +isOK>() +const data14 = new ArSyncModel({ field: 'currentUser', query: { posts: { params: { limit: 4 }, query: { id: true, title: true } } } }).data!.posts[0] +isOK>() +const data15 = new ArSyncModel({ field: 'currentUser', query: { posts: ['id', 'title'] } } as const).data!.posts[0] +isOK>() + +const data16 = new ArSyncModel({ field: 'currentUser', query: { id: { field: 'name' }, name: { field: 'id' }, id2: { field: 'id' }, name2: { field: 'name' } } }).data! +isOK>() +const data17 = new ArSyncModel({ field: 'currentUser', query: { posts: { '*': true, hoge: { field: 'comments', query: 'id' }, comments: { field: 'title' } } } }).data!.posts[0] +isOK>() +isOK>() diff --git a/tree/ArSyncStore.d.ts b/tree/ArSyncStore.d.ts index c28e267..fbfd5fe 100644 --- a/tree/ArSyncStore.d.ts +++ b/tree/ArSyncStore.d.ts @@ -1,8 +1,8 @@ export default class ArSyncStore { data: any; - query: any; + request: any; immutable: any; - constructor(query: any, data: any, option?: { + constructor(request: any, data: any, option?: { immutable?: boolean | undefined; }); replaceData(data: any): void; @@ -14,8 +14,12 @@ export default class ArSyncStore { changes: any; events: never[]; }; - _slicePatch(patchData: any, query: any): {}; + _slicePatch(patchData: any, query: any): any; _applyPatch(data: any, accessKeys: any, actualPath: any, updator: any, query: any, patchData: any): void; - _update(patch: any, updator: any, events: any): void; - static parseQuery(query: any, attrsonly?: any): {}; + _update(patch: { + action: string; + path: (string | number)[]; + ordering: any; + data: any; + }, updator: any, events: any): void; } diff --git a/tree/ArSyncStore.js b/tree/ArSyncStore.js index e66325b..1da59c3 100644 --- a/tree/ArSyncStore.js +++ b/tree/ArSyncStore.js @@ -1,5 +1,6 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); +const parseRequest_1 = require("../core/parseRequest"); class Updator { constructor(immutable) { this.changes = []; @@ -191,9 +192,9 @@ class Updator { } } class ArSyncStore { - constructor(query, data, option = {}) { + constructor(request, data, option = {}) { this.data = option.immutable ? Updator.createFrozenObject(data) : data; - this.query = ArSyncStore.parseQuery(query); + this.request = parseRequest_1.parseRequest(request); this.immutable = option.immutable; } replaceData(data) { @@ -210,28 +211,24 @@ class ArSyncStore { return this.batchUpdate([patch]); } _slicePatch(patchData, query) { - const obj = {}; - for (const key in patchData) { - if (key === 'id' || query.attributes['*']) { - obj[key] = patchData[key]; - } - else { - const subq = query.attributes[key]; - if (subq) { - obj[subq.column || key] = patchData[key]; - } - } + const obj = query && query['*'] ? Object.assign({}, patchData) : {}; + for (const key in query) { + const fieldQuery = query[key]; + const field = (fieldQuery && fieldQuery.field) || key; + if (field in patchData) + obj[key] = patchData[field]; } + if (patchData.id) + obj.id = patchData.id; return obj; } _applyPatch(data, accessKeys, actualPath, updator, query, patchData) { for (const key in patchData) { - const subq = query.attributes[key]; - const value = patchData[key]; - if (subq || query.attributes['*']) { - const subcol = (subq && subq.column) || key; - if (data[subcol] !== value) { - this.data = updator.add(this.data, accessKeys, actualPath, subcol, value); + const subq = query[key]; + const value = patchData[(subq && subq.field) || key]; + if (subq || query['*']) { + if (data[key] !== value) { + this.data = updator.add(this.data, accessKeys, actualPath, key, value); } } } @@ -239,127 +236,102 @@ class ArSyncStore { _update(patch, updator, events) { const { action, path } = patch; const patchData = patch.data; - let query = this.query; + let request = this.request; let data = this.data; - const actualPath = []; - const accessKeys = []; - for (let i = 0; i < path.length - 1; i++) { + const trace = (i, actualPath, accessKeys, query, data) => { const nameOrId = path[i]; + const lastStep = i === path.length - 1; if (typeof (nameOrId) === 'number') { const idx = data.findIndex(o => o.id === nameOrId); - if (idx < 0) - return; - actualPath.push(nameOrId); - accessKeys.push(idx); - data = data[idx]; + if (lastStep) { + apply(accessKeys, actualPath, query, null, idx, data[idx]); + } + else { + if (idx < 0) + return; + actualPath.push(nameOrId); + accessKeys.push(idx); + const data2 = data[idx]; + trace(i + 1, actualPath, accessKeys, query, data2); + } } else { - const { attributes } = query; - if (!attributes[nameOrId]) - return; - const column = attributes[nameOrId].column || nameOrId; - query = attributes[nameOrId]; - actualPath.push(column); - accessKeys.push(column); - data = data[column]; + const matchedKeys = []; + for (const key in query) { + const field = query[key].field || key; + if (field === nameOrId) + matchedKeys.push(key); + } + const fork = matchedKeys.length > 1; + for (const key of matchedKeys) { + const queryField = query[key]; + if (lastStep) { + if (!queryField) + return; + apply(accessKeys, actualPath, queryField.query, key, null, data[key]); + } + else { + const data2 = data[key]; + if (!data2) + return; + const actualPath2 = fork ? [...actualPath] : actualPath; + const accessKeys2 = fork ? [...accessKeys] : accessKeys; + actualPath2.push(key); + accessKeys2.push(key); + trace(i + 1, actualPath2, accessKeys2, queryField.query, data2); + } + } } - if (!data) + }; + const apply = (accessKeys, actualPath, query, column, idx, target) => { + if (action === 'create') { + const obj = this._slicePatch(patchData, query); + if (column) { + this.data = updator.add(this.data, accessKeys, actualPath, column, obj); + } + else if (!target) { + const ordering = Object.assign({}, patch.ordering); + const limitOverride = request.params && request.params.limit; + ordering.order = request.params && request.params.order || ordering.order; + if (ordering.limit == null || limitOverride != null && limitOverride < ordering.limit) + ordering.limit = limitOverride; + this.data = updator.add(this.data, accessKeys, actualPath, data.length, obj, ordering); + } return; - } - const nameOrId = path[path.length - 1]; - let id, idx, column, target = data; - if (typeof (nameOrId) === 'number') { - id = nameOrId; - idx = data.findIndex(o => o.id === id); - target = data[idx]; - } - else if (nameOrId) { - const { attributes } = query; - if (!attributes[nameOrId]) + } + if (action === 'destroy') { + if (column) { + this.data = updator.remove(this.data, accessKeys, actualPath, column); + } + else if (idx != null) { + this.data = updator.remove(this.data, accessKeys, actualPath, idx); + } + return; + } + if (!target) return; - column = attributes[nameOrId].column || nameOrId; - query = attributes[nameOrId]; - target = data[column]; - } - if (action === 'create') { - const obj = this._slicePatch(patchData, query); if (column) { - this.data = updator.add(this.data, accessKeys, actualPath, column, obj); + actualPath.push(column); + accessKeys.push(column); } - else if (!target) { - const ordering = Object.assign({}, patch.ordering); - const limitOverride = query.params && query.params.limit; - ordering.order = query.params && query.params.order || ordering.order; - if (ordering.limit == null || limitOverride != null && limitOverride < ordering.limit) - ordering.limit = limitOverride; - this.data = updator.add(this.data, accessKeys, actualPath, data.length, obj, ordering); + else if (idx != null && patchData.id) { + actualPath.push(patchData.id); + accessKeys.push(idx); } - return; - } - if (action === 'destroy') { - if (column) { - this.data = updator.remove(this.data, accessKeys, actualPath, column); + if (action === 'update') { + this._applyPatch(target, accessKeys, actualPath, updator, query, patchData); } - else if (idx >= 0) { - this.data = updator.remove(this.data, accessKeys, actualPath, idx); + else { + const eventData = { target, path: actualPath, data: patchData.data }; + events.push({ type: patchData.type, data: eventData }); } - return; - } - if (!target) - return; - if (column) { - actualPath.push(column); - accessKeys.push(column); - } - else if (id) { - actualPath.push(id); - accessKeys.push(idx); - } - if (action === 'update') { - this._applyPatch(target, accessKeys, actualPath, updator, query, patchData); + }; + if (path.length === 0) { + apply([], [], request.query, null, null, data); } else { - const eventData = { target, path: actualPath, data: patchData.data }; - events.push({ type: patchData.type, data: eventData }); - } - } - static parseQuery(query, attrsonly) { - const attributes = {}; - let column = null; - let params = null; - if (query.constructor !== Array) - query = [query]; - for (const arg of query) { - if (typeof (arg) === 'string') { - attributes[arg] = {}; - } - else if (typeof (arg) === 'object') { - for (const key in arg) { - const value = arg[key]; - if (attrsonly) { - attributes[key] = this.parseQuery(value); - continue; - } - if (key === 'attributes') { - const child = this.parseQuery(value, true); - for (const k in child) - attributes[k] = child[k]; - } - else if (key === 'as') { - column = value; - } - else if (key === 'params') { - params = value; - } - else { - attributes[key] = this.parseQuery(value); - } - } - } + trace(0, [], [], request.query, data); } - if (attrsonly) - return attributes; - return { attributes, column, params }; } } exports.default = ArSyncStore; diff --git a/vendor/assets/javascripts/ar_sync_graph.js.erb b/vendor/assets/javascripts/ar_sync_graph.js.erb index f05520c..55f6353 100644 --- a/vendor/assets/javascripts/ar_sync_graph.js.erb +++ b/vendor/assets/javascripts/ar_sync_graph.js.erb @@ -3,6 +3,8 @@ var modules = {} function require(name) { return modules[name] } <% require = -> (path) { path = File.dirname(__FILE__) + "/../../../#{path}"; depend_on path; File.read path } %> + <%= require.call 'core/parseRequest.js' %> + modules['../core/parseRequest'] = { parseRequest: exports.parseRequest } <%= require.call 'core/ArSyncApi.js' %> window.ArSyncAPI = exports.default modules['../core/ArSyncApi'] = { default: exports.default } diff --git a/vendor/assets/javascripts/ar_sync_tree.js.erb b/vendor/assets/javascripts/ar_sync_tree.js.erb index 45fafe6..5ac39f5 100644 --- a/vendor/assets/javascripts/ar_sync_tree.js.erb +++ b/vendor/assets/javascripts/ar_sync_tree.js.erb @@ -3,6 +3,8 @@ var modules = {} function require(name) { return modules[name] } <% require = -> (path) { path = File.dirname(__FILE__) + "/../../../#{path}"; depend_on path; File.read path } %> + <%= require.call 'core/parseRequest.js' %> + modules['../core/parseRequest'] = { parseRequest: exports.parseRequest } <%= require.call 'core/ArSyncApi.js' %> window.ArSyncAPI = exports.default modules['../core/ArSyncApi'] = { default: exports.default }