Skip to content

Commit

Permalink
Merge pull request #266 from mike-north/ts
Browse files Browse the repository at this point in the history
chore: typescript conversion
  • Loading branch information
mike-north committed Nov 1, 2018
2 parents 44ffad5 + 1a92733 commit a99e08f
Show file tree
Hide file tree
Showing 36 changed files with 1,049 additions and 908 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Expand Up @@ -42,7 +42,7 @@ install:
- yarn install --no-lockfile --non-interactive --skip-optional

script:
- yarn lint:js
- yarn lint:ts
# Usually, it's ok to finish the test scenario without reverting
# to the addon's original dependency state, skipping "cleanup".
- node_modules/.bin/ember try:one $EMBER_TRY_SCENARIO
Expand Down
2 changes: 1 addition & 1 deletion addon/index.js → addon/index.ts
@@ -1,5 +1,5 @@
import memberAction from './utils/member-action';
import collectionAction from './utils/collection-action';
import memberAction from './utils/member-action';
import serializeAndPush from './utils/serialize-and-push';

export const classOp = collectionAction;
Expand Down
47 changes: 26 additions & 21 deletions addon/utils/build-url.js → addon/utils/build-url.ts
@@ -1,23 +1,21 @@
/* eslint-disable no-unused-vars */
import Model from 'ember-data/model';
import Store from 'ember-data/store';
import { getOwner } from '@ember/application';
import EngineInstance from '@ember/engine/instance';
import DS from 'ember-data';
import Model from 'ember-data/model';
import { EmberDataRequestType } from './types';

/**
* Given a record, obtain the ember-data model class
* @param {Model} record
* @return {typeof Model}
* @param record
*/
export function _getModelClass(record) {
return /** @type {typeof Model} */ (record.constructor);
export function _getModelClass<M extends typeof Model>(record: InstanceType<M>): M {
return record.constructor as M;
}

/**
* Given an ember-data model class, obtain its name
* @param {typeof Model} clazz
* @returns {string}
* @param clazz
*/
export function _getModelName(clazz) {
export function _getModelName(clazz: typeof Model): string {
return (
// prettier-ignore
clazz.modelName // modern use
Expand All @@ -28,29 +26,36 @@ export function _getModelName(clazz) {

/**
* Given an ember-data-record, obtain the related Store
* @param {Model} record
* @return {Store}
* @param record
*/
export function _getStoreFromRecord(record) {
/** @type {EngineInstance} */
export function _getStoreFromRecord(record: Model) {
const owner = getOwner(record);
return owner.lookup('service:store');
}

function snapshotFromRecord(model: Model): DS.Snapshot {
return (model as any)._createSnapshot();
}

/**
*
* @param {Model} record
* @param {string} opPath
* @param {'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'} urlType
* @param {boolean} [instance]
* @param record
* @param opPath
* @param urlType
* @param instance
*/
export function buildOperationUrl(record, opPath, urlType, instance = true) {
export function buildOperationUrl<M extends Model>(
record: M,
opPath: string,
urlType: EmberDataRequestType,
instance = true
) {
const modelClass = _getModelClass(record);
const modelName = _getModelName(modelClass);
const store = _getStoreFromRecord(record);
const adapter = store.adapterFor(modelName);
const path = opPath;
const snapshot = record._createSnapshot();
const snapshot = snapshotFromRecord(record);
const baseUrl = adapter.buildURL(modelName, instance ? record.get('id') : null, snapshot, urlType);

if (!path) {
Expand Down
22 changes: 0 additions & 22 deletions addon/utils/collection-action.js

This file was deleted.

36 changes: 36 additions & 0 deletions addon/utils/collection-action.ts
@@ -0,0 +1,36 @@
import { merge } from '@ember/polyfills';
import Model from 'ember-data/model';
import { Value as JSONValue } from 'json-typescript';
import { _getModelClass, _getModelName, _getStoreFromRecord, buildOperationUrl } from './build-url';
import { EmberDataRequestType, Hook, HTTPVerb, strictifyHttpVerb } from './types';

export interface CollectionOperationOptions<IN, OUT> {
type?: HTTPVerb;
path: string;
urlType?: EmberDataRequestType;
ajaxOptions?: any;
before?: Hook<IN, any>;
after?: Hook<any, OUT>;
}

export default function collectionOp<IN = any, OUT = any>(options: CollectionOperationOptions<IN, OUT>) {
return function runCollectionOp(this: Model, payload: IN): Promise<OUT> {
const recordClass = _getModelClass(this);
const modelName = _getModelName(recordClass);
const store = _getStoreFromRecord(this);
const requestType: HTTPVerb = strictifyHttpVerb(options.type || 'put');
const urlType: EmberDataRequestType = options.urlType || 'updateRecord';
const adapter = store.adapterFor(modelName);
const fullUrl = buildOperationUrl(this, options.path, urlType, false);
const data = (options.before && options.before.call(this, payload)) || payload;
return adapter
.ajax(fullUrl, requestType, merge(options.ajaxOptions || {}, { data }))
.then((response: JSONValue) => {
if (options.after && !this.isDestroyed) {
return options.after.call(this, response);
}

return response;
});
};
}
22 changes: 0 additions & 22 deletions addon/utils/member-action.js

This file was deleted.

34 changes: 34 additions & 0 deletions addon/utils/member-action.ts
@@ -0,0 +1,34 @@
import { merge } from '@ember/polyfills';
import Model from 'ember-data/model';
import { Value as JSONValue } from 'json-typescript';
import { _getModelClass, _getModelName, _getStoreFromRecord, buildOperationUrl } from './build-url';
import { EmberDataRequestType, Hook, HTTPVerb, strictifyHttpVerb } from './types';

export interface InstanceOperationOptions<IN, OUT> {
type?: HTTPVerb;
path: string;
urlType?: EmberDataRequestType;
ajaxOptions?: any;
before?: Hook<IN, any>;
after?: Hook<any, OUT>;
}

export default function instanceOp<IN = any, OUT = any>(options: InstanceOperationOptions<IN, OUT>) {
return function runInstanceOp(this: Model, payload: IN): Promise<OUT> {
const recordClass = _getModelClass(this);
const modelName = _getModelName(recordClass);
const store = _getStoreFromRecord(this);
const { ajaxOptions, path, before, after, type = 'put', urlType = 'updateRecord' } = options;
const requestType: HTTPVerb = strictifyHttpVerb(type);
const adapter = store.adapterFor(modelName);
const fullUrl = buildOperationUrl(this, path, urlType);
const data = (before && before.call(this, payload)) || payload;
return adapter.ajax(fullUrl, requestType, merge(ajaxOptions || {}, { data })).then((response: JSONValue) => {
if (after && !this.isDestroyed) {
return after.call(this, response);
}

return response;
});
};
}
19 changes: 0 additions & 19 deletions addon/utils/serialize-and-push.js

This file was deleted.

45 changes: 45 additions & 0 deletions addon/utils/serialize-and-push.ts
@@ -0,0 +1,45 @@
import { isArray } from '@ember/array';
import { typeOf } from '@ember/utils';
import Model from 'ember-data/model';
import JSONSerializer from 'ember-data/serializers/json';
import SerializerRegistry from 'ember-data/types/registries/serializer';
import { CollectionResourceDoc, Document as JSONApiDoc, DocWithData, SingleResourceDoc } from 'jsonapi-typescript';
import { _getModelClass, _getModelName, _getStoreFromRecord } from './build-url';

function isJsonApi(raw: any): raw is JSONApiDoc {
return raw.jsonapi && raw.jsonapi.version;
}
function isDocWithData(doc: any): doc is DocWithData {
return isJsonApi(doc) && ['object', 'array'].indexOf(typeOf((doc as DocWithData).data)) >= 0;
}

export default function serializeAndPush(this: Model, response: any) {
if (!isDocWithData(response)) {
// tslint:disable-next-line:no-console
console.warn(
'serializeAndPush may only be used with a JSON API document. Ignoring response. ' +
'Document must have a mandatory JSON API object. See https://jsonapi.org/format/#document-jsonapi-object.'
);
return response;
}

const recordClass = _getModelClass(this);
const modelName = _getModelName(recordClass);
const store = _getStoreFromRecord(this);
const serializer: JSONSerializer = store.serializerFor(modelName as keyof SerializerRegistry);
let normalized: {};
if (isArray(response.data)) {
const doc = response as CollectionResourceDoc;
normalized = serializer.normalizeArrayResponse(store, recordClass as any, doc, null as any, 'findAll');
} else {
const doc = response as SingleResourceDoc;
normalized = serializer.normalizeSingleResponse(
store,
recordClass as any,
doc,
`${doc.data.id || '(unknown)'}`,
'findRecord'
);
}
return store.push(normalized);
}
53 changes: 53 additions & 0 deletions addon/utils/types.ts
@@ -0,0 +1,53 @@
import Model from 'ember-data/model';

export type StrictHTTPVerb = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS' | 'HEAD';
export type HTTPVerb =
| 'GET'
| 'POST'
| 'PUT'
| 'DELETE'
| 'PATCH'
| 'OPTIONS'
| 'HEAD'
| 'get'
| 'post'
| 'put'
| 'delete'
| 'patch'
| 'options'
| 'head';

export interface HTTPVerbStrictMap {
GET: 'GET';
POST: 'POST';
PUT: 'PUT';
DELETE: 'DELETE';
PATCH: 'PATCH';
OPTIONS: 'OPTIONS';
HEAD: 'HEAD';
get: 'GET';
post: 'POST';
put: 'PUT';
delete: 'DELETE';
patch: 'PATCH';
options: 'OPTIONS';
head: 'HEAD';
}

export function strictifyHttpVerb<K extends keyof HTTPVerbStrictMap>(notStrict: K): HTTPVerbStrictMap[K] {
return `${notStrict}`.toUpperCase() as HTTPVerbStrictMap[K];
}

export type EmberDataRequestType =
| 'findRecord'
| 'findAll'
| 'query'
| 'queryRecord'
| 'findMany'
| 'findHasMany'
| 'findBelongsTo'
| 'createRecord'
| 'updateRecord'
| 'deleteRecord';

export type Hook<IN, OUT> = (this: Model, payload: IN) => OUT;
1 change: 0 additions & 1 deletion app/util-tests/collection-action.js

This file was deleted.

1 change: 0 additions & 1 deletion app/util-tests/member-action.js

This file was deleted.

Binary file added ember__object.tgz
Binary file not shown.

0 comments on commit a99e08f

Please sign in to comment.