Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/silver-impalas-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@aws-amplify/data-schema': minor
---

feat: support field-level resolvers to be added to the model
5 changes: 5 additions & 0 deletions packages/data-schema/__tests__/ModelField.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import {
Json,
type BaseModelField,
} from '../src/ModelField';
import { defineFunctionStub } from './utils';
import { a } from '../src/index';

/**
* Extracts first type arg from ModelField
Expand Down Expand Up @@ -53,6 +55,9 @@ test('string() produces expected type args', () => {
type FieldReqArrayReq = GetFieldTypeArg<typeof fieldReqArrayReq>;

type optArrOptTest = Expect<Equal<FieldReqArrayReq, Array<string>>>;

const handler = defineFunctionStub({});
string().handler(a.handler.function(handler));
});

test('ModelField can be cast to InternalField', () => {
Expand Down
74 changes: 74 additions & 0 deletions packages/data-schema/__tests__/ModelField.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ import {
InternalRelationshipField,
} from '../src/ModelRelationshipField';
import { Authorization, ImpliedAuthFields } from '../src/Authorization';
import { defineFunctionStub } from './utils';
import { configure } from '../src/internals';

const fakeSecret = () => ({}) as any;

const datasourceConfigMySQL = {
engine: 'mysql',
connectionUri: fakeSecret(),
} as const;

const aSql = configure({ database: datasourceConfigMySQL });

// evaluates type defs in corresponding test-d.ts file
it('should not produce static type errors', async () => {
Expand Down Expand Up @@ -100,6 +111,69 @@ describe('field level auth', () => {
});
});

describe('field handlers', () => {
it('should allow adding a lambda handler to a field', () => {
const handler = defineFunctionStub({});
const s = a
.schema({
Asset: a.model({
content: a.string().handler(a.handler.function(handler)),
}).authorization((allow) => allow.publicApiKey()),
});
const result = s.transform().schema;
expect(result).toMatchSnapshot();
});

it('should allow adding multiple handlers to a field', () => {
const handler = defineFunctionStub({});
const handler2 = defineFunctionStub({});
const s = a
.schema({
Asset: a.model({
content: a.string().handler([a.handler.function(handler), a.handler.function(handler2)]),
}).authorization((allow) => allow.publicApiKey()),
});
const result = s.transform().schema;
expect(result).toMatchSnapshot();
});

it('should allow adding a handler with auth to a field', () => {
const handler = defineFunctionStub({});
const s = a
.schema({
Asset: a.model({
content: a
.string()
.handler(a.handler.function(handler)).authorization((allow) => allow.authenticated()),
}).authorization((allow) => allow.publicApiKey()),
});
const result = s.transform().schema;
expect(result).toMatchSnapshot();
});

it('should allow adding a inline sql handler to a field', () => {
const s = aSql
.schema({
Asset: a.model({
content: a.string().handler(a.handler.inlineSql('SELECT content FROM TESTTABLE;')),
}).authorization((allow) => allow.publicApiKey()),
});
const result = s.transform().schema;
expect(result).toMatchSnapshot();
});

it('should allow adding a sql reference handler to a field', () => {
const s = aSql
.schema({
Asset: a.model({
content: a.string().handler(a.handler.sqlReference('getContent.sql')),
}).authorization((allow) => allow.publicApiKey()),
});
const result = s.transform().schema;
expect(result).toMatchSnapshot();
});
});

it('array modifier becomes unavailable after being used once', () => {
// @ts-expect-error .array() is not a valid modifier after being used once
a.model({ values: a.string().required().array().required().array().required() });
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,40 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`field handlers should allow adding a handler with auth to a field 1`] = `
"type Asset @model @auth(rules: [{allow: public, provider: apiKey}])
{
content: String @function(name: "FnAssetContent") @auth(rules: [{allow: private}])
}"
`;

exports[`field handlers should allow adding a inline sql handler to a field 1`] = `
"type Asset @model(timestamps: null) @auth(rules: [{allow: public, provider: apiKey}])
{
content: String @sql(statement: "SELECT content FROM TESTTABLE;")
}"
`;

exports[`field handlers should allow adding a lambda handler to a field 1`] = `
"type Asset @model @auth(rules: [{allow: public, provider: apiKey}])
{
content: String @function(name: "FnAssetContent")
}"
`;

exports[`field handlers should allow adding a sql reference handler to a field 1`] = `
"type Asset @model(timestamps: null) @auth(rules: [{allow: public, provider: apiKey}])
{
content: String @sql(reference: "getContent.sql")
}"
`;

exports[`field handlers should allow adding multiple handlers to a field 1`] = `
"type Asset @model @auth(rules: [{allow: public, provider: apiKey}])
{
content: String @function(name: "FnAssetContent") @function(name: "FnAssetContent2")
}"
`;

exports[`field level auth implied fields objects can be extracted 1`] = `
[
{
Expand Down
1 change: 1 addition & 0 deletions packages/data-schema/docs/data-schema.modelfield.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export type ModelField<T extends ModelFieldTypeParamOuter = ModelFieldTypeParamO
default(value?: ModelFieldTypeParamOuter): ModelField<T, UsedMethod | 'default', Auth, FT>;
authorization<AuthRuleType extends Authorization<any, any, any>>(callback: (allow: Omit<AllowModifier, 'resource'>) => AuthRuleType | AuthRuleType[]): ModelField<T, UsedMethod | 'authorization', AuthRuleType, FT>;
validate(callback: (v: FieldTypeToValidationBuilder<T, FT>) => void): ModelField<T, UsedMethod | 'validate' | 'default' | 'array', Auth, FT>;
handler(handlers: HandlerInputType): ModelField<T, UsedMethod | 'handler', Auth, FT>;
}, UsedMethod>;
```
**References:** [ModelFieldType](./data-schema.modelfieldtype.md)<!-- -->, [ModelField](./data-schema.modelfield.md)<!-- -->, [Authorization](./data-schema.authorization.md)
Expand Down
23 changes: 22 additions & 1 deletion packages/data-schema/src/ModelField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ import {
FieldTypeToValidationBuilder,
createValidationBuilder,
} from './Validate';
import { AsyncFunctionHandler, CustomHandler, FunctionHandler, HandlerType as Handler } from './Handler';

type FieldHandler = Exclude<Handler, AsyncFunctionHandler>;

type HandlerInputType =
| FunctionHandler[]
| CustomHandler[]
| FieldHandler;

/**
* Used to "attach" auth types to ModelField without exposing them on the builder.
Expand Down Expand Up @@ -58,6 +66,7 @@ type FieldData = {
default: undefined | symbol | ModelFieldTypeParamOuter;
authorization: Authorization<any, any, any>[];
validation: ValidationRule[];
handlers: FieldHandler[];
};

type ModelFieldTypeParamInner = string | number | boolean | Date | Json | null;
Expand Down Expand Up @@ -90,7 +99,7 @@ export type BaseModelField<

export type UsableModelFieldKey = satisfy<
methodKeyOf<ModelField>,
'required' | 'default' | 'authorization' | 'array' | 'validate'
'required' | 'default' | 'authorization' | 'array' | 'validate' | 'handler'
>;

/**
Expand Down Expand Up @@ -165,6 +174,11 @@ export type ModelField<
validate(
callback: (v: FieldTypeToValidationBuilder<T, FT>) => void
): ModelField<T, UsedMethod | 'validate' | 'default' | 'array', Auth, FT>;
/**
* Configures field-level handlers.
* @param handlers - An array of handler functions to be invoked for this field
*/
handler(handlers: HandlerInputType): ModelField<T, UsedMethod | 'handler', Auth, FT>;
},
UsedMethod
>;
Expand Down Expand Up @@ -209,6 +223,7 @@ function _field<T extends ModelFieldTypeParamOuter, FT extends ModelFieldType>(
default: undefined,
authorization: [],
validation: [],
handlers: [],
};

const builder = {
Expand Down Expand Up @@ -254,6 +269,12 @@ function _field<T extends ModelFieldTypeParamOuter, FT extends ModelFieldType>(

return this;
},
handler(handlers: HandlerInputType) {
data.handlers = Array.isArray(handlers) ? handlers : [handlers] as FieldHandler[];
_meta.lastInvokedMethod = 'handler';

return this;
},
...brand(brandName),
[internal]() {
return this;
Expand Down
Loading