Skip to content

Commit

Permalink
feat: findRecord and query request builders (#8687)
Browse files Browse the repository at this point in the history
* WIP Starting out request-utils package

* updates to gitignore and tracking version

* new contributing docs for identity and basic implementation for buildURL and JSON:API findREcord builder

* update builders with REST builder for findRecord

* fix pnpm install

* implement query params serialization with Jake and Chris

* rename to buildQueryParams

* refactor JSON:API findRecord op into multiple files

* update factoring for REST

* add test harness and some basic tests

* ensure tests run

* update tests

* add tests for buildQueryParams

* tests for builders

* adds ActiveRecord builder support

* fix root config

* fix root config

---------

Co-authored-by: Jacob Beltran <[email protected]>
Co-authored-by: Natasha Wolfe <[email protected]>
  • Loading branch information
3 people authored Jul 8, 2023
1 parent 8033558 commit 4237849
Show file tree
Hide file tree
Showing 73 changed files with 2,965 additions and 675 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ packages/model/addon
packages/json-api/addon
packages/graph/addon
packages/legacy-compat/addon
packages/request-utils/addon
packages/rest/addon
packages/active-record/addon

# dependencies
bower_components
Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ detailing how to become involved to best ensure your contributions are successfu
- [Requesting Features or Deprecations](./contributing/rfc-process.md)
- [Submitting Pull Requests](./contributing/submitting-prs.md)
- [Linking the project to your application locally](./contributing/linking-to-applications.md)
- [Key Concepts](./contributing/key-concepts.md)

You may also want to review the [roadmap](./ROADMAP.md) for ideas of how you may want to get
involved.
36 changes: 36 additions & 0 deletions contributing/key-concepts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Key Concepts

- [Identity](#-identity)

-------------

## 🔸 Identity

### Working with Identifiers and TypeScript

Identifying information can be encountered in several different manners depending on which APIs are being worked with and whether the code is "internal" or "public facing".

* The "ResourceIdentifier" type is used when the identifying information is end-user-supplied and not guaranteed to have "lid".

Example: `findRecord({ type: 'user', id: '1' })`

Most commonly this is the case when a record has not been encountered yet or when processing a payload received from the API.

* The "RecordIdentifier" type is used when identifying information MUST have "lid" but may not be the "stable" identifier object instance.

Example: `saveRecord({ type: 'user', id: null, lid: 'user:1' })`

Most commonly this is the case when the user might manually construct an identifier. Often this is the result of having previously serialized record state and later attempting to restore it.

* The "StableRecordIdentifier" type is used when identifying information MUST have "lid" AND MUST be the "stable" identifier object
instance produced and managed by the `IdentifierCache` associated to a
specific `Store` instance.

Example:

```ts
const identifier = recordIdentifierFor(record);
unloadRecord(identifier);
```

Any identifier supplied by an EmberData API will always be the stable variant. APIs which are operating based on identity and which can reasonably presume that the data exists expect stable identifiers and should error if an unknown identifier is encountered to prevent potential system-correctness errors.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"lint:js": "eslint --quiet --cache --cache-strategy=content --ext=js,ts .",
"preinstall": "npx only-allow pnpm",
"problems": "tsc -p tsconfig.json --noEmit --pretty false",
"test": "pnpm --filter main-test-app --filter graph-test-app --filter json-api-test-app --filter request-test-app run test",
"test": "pnpm --filter main-test-app --filter graph-test-app --filter json-api-test-app --filter request-test-app --filter builders-test-app run test",
"test:production": "pnpm --filter main-test-app --filter graph-test-app --filter json-api-test-app run test -e production",
"test:try-one": "pnpm --filter main-test-app run test:try-one",
"test:docs": "pnpm build:docs && pnpm --filter docs-tests test",
Expand Down
11 changes: 11 additions & 0 deletions packages/active-record/LICENSE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
The MIT License (MIT)

Copyright (C) 2017-2023 Ember.js contributors
Portions Copyright (C) 2011-2017 Tilde, Inc. and contributors.
Portions Copyright (C) 2011 LivingSocial Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
13 changes: 13 additions & 0 deletions packages/active-record/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
@ember-data/active-record
============================================================================

ActiveRecord Format Support for EmberData

> Note: This is a V2 Addon, but we have intentionally configured it to act and report as a V1 Addon due
to bugs with ember-auto-import.
>
> We can remove the V1 tag if ember-auto-import will no longer attempt
to load V2 addons or if it is fixed to work with V1 addons with custom addon trees and also dedupes modules for test apps.
>
> You can still consume this as a normal library.
> In other projects.
19 changes: 19 additions & 0 deletions packages/active-record/addon-main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module.exports = {
name: require('./package.json').name,

treeForVendor() {
return;
},
treeForPublic() {
return;
},
treeForStyles() {
return;
},
treeForAddonStyles() {
return;
},
treeForApp() {
return;
},
};
8 changes: 8 additions & 0 deletions packages/active-record/babel.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"plugins": [
"@babel/plugin-transform-runtime",
["@babel/plugin-transform-typescript", { "allowDeclareFields": true }],
["@babel/plugin-proposal-decorators", { "legacy": true }],
"@babel/plugin-proposal-class-properties"
]
}
70 changes: 70 additions & 0 deletions packages/active-record/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
{
"name": "@ember-data/active-record",
"description": "ActiveRecord Format Support for EmberData",
"version": "5.3.0-alpha.3",
"private": false,
"license": "MIT",
"author": "Chris Thoburn <[email protected]>",
"repository": {
"type": "git",
"url": "git+ssh://[email protected]:emberjs/data.git",
"directory": "packages/active-record"
},
"homepage": "https://github.com/emberjs/data",
"bugs": "https://github.com/emberjs/data/issues",
"engines": {
"node": "16.* || >= 18"
},
"keywords": [
"ember-addon"
],
"volta": {
"extends": "../../package.json"
},
"dependencies": {
"ember-cli-babel": "^7.26.11"
},
"peerDependencies": {
"ember-inflector": "^4.0.2"
},
"files": [
"addon-main.js",
"addon",
"README.md",
"LICENSE.md",
"ember-data-logo-dark.svg",
"ember-data-logo-light.svg"
],
"scripts": {
"build": "rollup --config && babel ./addon --out-dir addon --plugins=../private-build-infra/src/transforms/babel-plugin-transform-ext.js",
"start": "rollup --config --watch",
"prepack": "pnpm build",
"prepare": "pnpm build"
},
"ember-addon": {
"main": "addon-main.js",
"type": "addon",
"version": 1
},
"devDependencies": {
"@babel/core": "^7.21.8",
"@babel/cli": "^7.21.5",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-proposal-decorators": "^7.21.0",
"@babel/plugin-transform-runtime": "^7.21.4",
"@babel/plugin-transform-typescript": "^7.21.3",
"@babel/preset-env": "^7.21.5",
"@babel/preset-typescript": "^7.21.5",
"@babel/runtime": "^7.21.5",
"@embroider/addon-dev": "^3.0.0",
"@rollup/plugin-babel": "^6.0.3",
"@rollup/plugin-node-resolve": "^15.0.2",
"rollup": "^3.21.7",
"tslib": "^2.5.0",
"typescript": "^5.0.4",
"walk-sync": "^3.0.0"
},
"ember": {
"edition": "octane"
}
}
31 changes: 31 additions & 0 deletions packages/active-record/rollup.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Addon } from '@embroider/addon-dev/rollup';
import babel from '@rollup/plugin-babel';
import { nodeResolve } from '@rollup/plugin-node-resolve';

const addon = new Addon({
srcDir: 'src',
destDir: 'addon',
});

export default {
// This provides defaults that work well alongside `publicEntrypoints` below.
// You can augment this if you need to.
output: addon.output(),

external: [],

plugins: [
// These are the modules that users should be able to import from your
// addon. Anything not listed here may get optimized away.
addon.publicEntrypoints(['request.js']),

nodeResolve({ extensions: ['.ts'] }),
babel({
extensions: ['.ts'],
babelHelpers: 'runtime', // we should consider "external",
}),

// Remove leftover build artifacts when starting a new build.
addon.clean(),
],
};
39 changes: 39 additions & 0 deletions packages/active-record/src/-private/builders/-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { QueryParamsSerializationOptions } from '@ember-data/request-utils';
import type { ResourceIdentifierObject } from '@ember-data/types/q/ember-data-json-api';

export type CacheOptions = {
key?: string;
reload?: boolean;
backgroundReload?: boolean;
};
export type FindRecordRequestOptions = {
url: string;
method: 'GET';
headers: Headers;
cacheOptions: CacheOptions;
op: 'findRecord';
records: [ResourceIdentifierObject];
};

export type QueryRequestOptions = {
url: string;
method: 'GET';
headers: Headers;
cacheOptions: CacheOptions;
op: 'query';
};

export type RemotelyAccessibleIdentifier = {
id: string;
type: string;
lid?: string;
};

export type ConstrainedRequestOptions = {
reload?: boolean;
backgroundReload?: boolean;
host?: string;
namespace?: string;
resourcePath?: string;
urlParamsSettings?: QueryParamsSerializationOptions;
};
26 changes: 26 additions & 0 deletions packages/active-record/src/-private/builders/-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { type UrlOptions } from '@ember-data/request-utils';

import type { CacheOptions, ConstrainedRequestOptions } from './-types';

export function copyForwardUrlOptions(urlOptions: UrlOptions, options: ConstrainedRequestOptions): void {
if ('host' in options) {
urlOptions.host = options.host;
}
if ('namespace' in options) {
urlOptions.namespace = options.namespace;
}
if ('resourcePath' in options) {
urlOptions.resourcePath = options.resourcePath;
}
}

export function extractCacheOptions(options: ConstrainedRequestOptions) {
const cacheOptions: CacheOptions = {};
if ('reload' in options) {
cacheOptions.reload = options.reload;
}
if ('backgroundReload' in options) {
cacheOptions.backgroundReload = options.backgroundReload;
}
return cacheOptions;
}
49 changes: 49 additions & 0 deletions packages/active-record/src/-private/builders/find-record.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { underscore } from '@ember/string';

import { pluralize } from 'ember-inflector';

import { buildBaseURL, buildQueryParams, type FindRecordUrlOptions } from '@ember-data/request-utils';

import type { ConstrainedRequestOptions, FindRecordRequestOptions, RemotelyAccessibleIdentifier } from './-types';
import { copyForwardUrlOptions, extractCacheOptions } from './-utils';

type FindRecordOptions = ConstrainedRequestOptions & {
include?: string | string[];
};

export function findRecord(
identifier: RemotelyAccessibleIdentifier,
options?: FindRecordOptions
): FindRecordRequestOptions;
export function findRecord(type: string, id: string, options?: FindRecordOptions): FindRecordRequestOptions;
export function findRecord(
arg1: string | RemotelyAccessibleIdentifier,
arg2: string | FindRecordOptions | undefined,
arg3?: FindRecordOptions
): FindRecordRequestOptions {
const identifier: RemotelyAccessibleIdentifier = typeof arg1 === 'string' ? { type: arg1, id: arg2 as string } : arg1;
const options = ((typeof arg1 === 'string' ? arg3 : arg2) || {}) as FindRecordOptions;
const cacheOptions = extractCacheOptions(options);
const urlOptions: FindRecordUrlOptions = {
identifier,
op: 'findRecord',
resourcePath: pluralize(underscore(identifier.type)),
};

copyForwardUrlOptions(urlOptions, options);

const url = buildBaseURL(urlOptions);
const headers = new Headers();
headers.append('Content-Type', 'application/json; charset=utf-8');

return {
url: options.include?.length
? `${url}?${buildQueryParams({ include: options.include }, options.urlParamsSettings)}`
: url,
method: 'GET',
headers,
cacheOptions,
op: 'findRecord',
records: [identifier],
};
}
35 changes: 35 additions & 0 deletions packages/active-record/src/-private/builders/query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { underscore } from '@ember/string';

import { pluralize } from 'ember-inflector';

import { buildBaseURL, buildQueryParams, QueryParamsSource, type QueryUrlOptions } from '@ember-data/request-utils';

import type { ConstrainedRequestOptions, QueryRequestOptions } from './-types';
import { copyForwardUrlOptions, extractCacheOptions } from './-utils';

export function query(
type: string,
query: QueryParamsSource = {},
options: ConstrainedRequestOptions = {}
): QueryRequestOptions {
const cacheOptions = extractCacheOptions(options);
const urlOptions: QueryUrlOptions = {
identifier: { type },
op: 'query',
resourcePath: pluralize(underscore(type)),
};

copyForwardUrlOptions(urlOptions, options);

const url = buildBaseURL(urlOptions);
const headers = new Headers();
headers.append('Content-Type', 'application/json; charset=utf-8');

return {
url: `${url}?${buildQueryParams(query, options.urlParamsSettings)}`,
method: 'GET',
headers,
cacheOptions,
op: 'query',
};
}
Empty file.
2 changes: 2 additions & 0 deletions packages/active-record/src/request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { findRecord } from './-private/builders/find-record';
export { query } from './-private/builders/query';
3 changes: 2 additions & 1 deletion packages/json-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
],
"peerDependencies": {
"@ember-data/store": "workspace:5.3.0-alpha.3",
"@ember-data/graph": "workspace:5.3.0-alpha.3"
"@ember-data/graph": "workspace:5.3.0-alpha.3",
"ember-inflector": "^4.0.2"
},
"dependenciesMeta": {
"@ember-data/private-build-infra": {
Expand Down
2 changes: 1 addition & 1 deletion packages/json-api/rollup.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export default {
plugins: [
// These are the modules that users should be able to import from your
// addon. Anything not listed here may get optimized away.
addon.publicEntrypoints(['index.js']),
addon.publicEntrypoints(['index.js', 'request.js']),

nodeResolve({ extensions: ['.ts', '.js'] }),
babel({
Expand Down
Loading

0 comments on commit 4237849

Please sign in to comment.