Skip to content
Open
5 changes: 5 additions & 0 deletions .changeset/fair-knives-sort.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@aws-amplify/data-schema': minor
---

Add a.AuthorizationCallback authorization sharing support
198 changes: 198 additions & 0 deletions packages/data-schema/__tests__/Authorization.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import { DefineFunction } from "@aws-amplify/data-schema-types";
import { a } from "../src";
import { AuthorizationCallback } from "../src/Authorization";

describe('AuthorizationCallback definition', () => {
test('customOperation is compatible with all types except conversations', () => {
const callback: AuthorizationCallback<'customOperation'> = (a) => {
[
a.authenticated(),
a.custom('function'),
a.group('test'),
a.groups(['testGroup']),
a.guest(),
a.publicApiKey()
]
};

const customOpAuth = a.schema({M: a.query().authorization(callback)});
const fieldAuth = a.schema({M: a.model({field: a.string().authorization(callback)})});
const relationshipAuth = a.schema({Y: a.model({}), M: a.model({ys: a.hasMany('Y', 'yid').authorization(callback)})});
const modelAuth = a.schema({M: a.model({}).authorization(callback)});
const refAuth = a.schema({R: a.customType({t: a.string()}), M: a.model({field: a.ref('R').authorization(callback)})});
const schemaAuth = a.schema({M: a.model({})}).authorization(callback);
// @ts-expect-error customOperation is incompatible with conversation
const conversationAuth = a.schema({chat: a.conversation({aiModel: a.ai.model("Claude 3 Opus"), systemPrompt: ''}).authorization(callback)});
});

test('field is compatible with all types except custom operations and conversations', () => {
// `model` is the default callback type as it is featurefull and compatable with the most
// This is the same as `AuthorizationCallback<'model'>`
const callback: AuthorizationCallback<'field'> = (a) => [
a.authenticated(),
a.custom('function'),
a.group('test'),
a.groupDefinedIn('group'),
a.groups(['testGroup']),
a.groupsDefinedIn('groups'),
a.guest(),
a.owner(),
a.ownerDefinedIn('owner'),
a.ownersDefinedIn('owners'),
a.publicApiKey(),
];

// @ts-expect-error field is incompatible with custom operations
const customOpAuth = a.schema({M: a.query().authorization(callback)});
const fieldAuth = a.schema({M: a.model({field: a.string().authorization(callback)})});
const relationshipAuth = a.schema({Y: a.model({}), M: a.model({ys: a.hasMany('Y', 'yid').authorization(callback)})});
const modelAuth = a.schema({M: a.model({}).authorization(callback)});
const refAuth = a.schema({R: a.customType({t: a.string()}), M: a.model({field: a.ref('R').authorization(callback)})});
const schemaAuth = a.schema({M: a.model({})}).authorization(callback);
// @ts-expect-error field is incompatible with conversation
const conversationAuth = a.schema({chat: a.conversation({aiModel: a.ai.model("Claude 3 Opus"), systemPrompt: ''}).authorization(callback)});
});

test('relationship is compatible with all types except custom operations, field, model and conversations', () => {
const callback: AuthorizationCallback<'relationship'> = (a) => [
a.authenticated(),
a.custom('function'),
a.group('test'),
a.groupDefinedIn('group'),
a.groups(['testGroup']),
a.groupsDefinedIn('groups'),
a.guest(),
a.owner(),
a.ownerDefinedIn('owner'),
a.ownersDefinedIn('owners'),
a.publicApiKey(),
a.resource({} as DefineFunction),
];

// @ts-expect-error relationship is incompatible with custom operations
const customOpAuth = a.schema({M: a.query().authorization(callback)});
// @ts-expect-error relationship is incompatible with field
const fieldAuth = a.schema({M: a.model({field: a.string().authorization(callback)})});
const relationshipAuth = a.schema({Y: a.model({}), M: a.model({ys: a.hasMany('Y', 'yid').authorization(callback)})});
// @ts-expect-error relationship is incompatible with model
const modelAuth = a.schema({M: a.model({}).authorization(callback)});
const refAuth = a.schema({R: a.customType({t: a.string()}), M: a.model({field: a.ref('R').authorization(callback)})});
const schemaAuth = a.schema({M: a.model({})}).authorization(callback);
// @ts-expect-error relationship is incompatible with conversation
const conversationAuth = a.schema({chat: a.conversation({aiModel: a.ai.model("Claude 3 Opus"), systemPrompt: ''}).authorization(callback)});
});

test('model is compatible with all types except custom operations and conversations', () => {
const callback: AuthorizationCallback = (a) => {
[
a.authenticated(),
a.custom('function'),
a.group('test'),
a.groupDefinedIn('group'),
a.groups(['testGroup']),
a.groupsDefinedIn('groups'),
a.guest(),
a.owner(),
a.ownerDefinedIn('owner'),
a.ownersDefinedIn('owners'),
a.publicApiKey(),
]
};

// @ts-expect-error model is incompatible with custom operations
const customOpAuth = a.schema({M: a.query().authorization(callback)});
const fieldAuth = a.schema({M: a.model({field: a.string().authorization(callback)})});
const relationshipAuth = a.schema({Y: a.model({}), M: a.model({ys: a.hasMany('Y', 'yid').authorization(callback)})});
const modelAuth = a.schema({M: a.model({}).authorization(callback)});
const refAuth = a.schema({R: a.customType({t: a.string()}), M: a.model({field: a.ref('R').authorization(callback)})});
const schemaAuth = a.schema({M: a.model({})}).authorization(callback);
// @ts-expect-error model is incompatible with conversation
const conversationAuth = a.schema({chat: a.conversation({aiModel: a.ai.model("Claude 3 Opus"), systemPrompt: ''}).authorization(callback)});
});

test('ref is compatible with all types except custom operations, field, model and conversations', () => {
const callback: AuthorizationCallback<'ref'> = (a) => {
[
a.authenticated(),
a.custom('function'),
a.group('test'),
a.groupDefinedIn('group'),
a.groups(['testGroup']),
a.groupsDefinedIn('groups'),
a.guest(),
a.owner(),
a.ownerDefinedIn('owner'),
a.ownersDefinedIn('owners'),
a.publicApiKey(),
a.resource({} as DefineFunction),
]
};

// @ts-expect-error ref is incompatible with custom operations
const customOpAuth = a.schema({M: a.query().authorization(callback)});
// @ts-expect-error ref is incompatible with field
const fieldAuth = a.schema({M: a.model({field: a.string().authorization(callback)})});
const relationshipAuth = a.schema({Y: a.model({}), M: a.model({ys: a.hasMany('Y', 'yid').authorization(callback)})});
// @ts-expect-error ref is incompatible with model
const modelAuth = a.schema({M: a.model({}).authorization(callback)});
const refAuth = a.schema({R: a.customType({t: a.string()}), M: a.model({field: a.ref('R').authorization(callback)})});
const schemaAuth = a.schema({M: a.model({})}).authorization(callback);
// @ts-expect-error ref is incompatible with conversation
const conversationAuth = a.schema({chat: a.conversation({aiModel: a.ai.model("Claude 3 Opus"), systemPrompt: ''}).authorization(callback)});
});

test('schema is compatible with all types except custom operations, field, model and conversations', () => {
const callback: AuthorizationCallback<'schema'> = (a) => {
[
a.authenticated(),
a.custom('function'),
a.group('test'),
a.groupDefinedIn('group'),
a.groups(['testGroup']),
a.groupsDefinedIn('groups'),
a.guest(),
a.owner(),
a.ownerDefinedIn('owner'),
a.ownersDefinedIn('owners'),
a.publicApiKey(),
a.resource({} as DefineFunction),
]
};

// @ts-expect-error schema is incompatible with custom operations
const customOpAuth = a.schema({M: a.query().authorization(callback)});
// @ts-expect-error schema is incompatible with field
const fieldAuth = a.schema({M: a.model({field: a.string().authorization(callback)})});
const relationshipAuth = a.schema({Y: a.model({}), M: a.model({ys: a.hasMany('Y', 'yid').authorization(callback)})});
// @ts-expect-error schema is incompatible with model
const modelAuth = a.schema({M: a.model({}).authorization(callback)});
const refAuth = a.schema({R: a.customType({t: a.string()}), M: a.model({field: a.ref('R').authorization(callback)})});
const schemaAuth = a.schema({M: a.model({})}).authorization(callback);
// @ts-expect-error schema is incompatible with conversation
const conversationAuth = a.schema({chat: a.conversation({aiModel: a.ai.model("Claude 3 Opus"), systemPrompt: ''}).authorization(callback)});
});

test('conversation is only compatible with conversations', () => {
// The owner behavior used elsewhere include a provider override `provider?: OwnerProviders`
// This is not supported by conversations and makes the types entirely incompatible with other owner authorization types
const callback: AuthorizationCallback<'conversation'> = (a) => {
[
a.owner(),
]
};

// @ts-expect-error conversation is incompatible with custom operations
const customOpAuth = a.schema({M: a.query().authorization(callback)});
// @ts-expect-error conversation is incompatible with field
const fieldAuth = a.schema({M: a.model({field: a.string().authorization(callback)})});
// @ts-expect-error conversation is incompatible with relationship
const relationshipAuth = a.schema({Y: a.model({}), M: a.model({ys: a.hasMany('Y', 'yid').authorization(callback)})});
// @ts-expect-error conversation is incompatible with model
const modelAuth = a.schema({M: a.model({}).authorization(callback)});
// @ts-expect-error conversation is incompatible with ref
const refAuth = a.schema({R: a.customType({t: a.string()}), M: a.model({field: a.ref('R').authorization(callback)})});
// @ts-expect-error conversation is incompatible with schema
const schemaAuth = a.schema({M: a.model({})}).authorization(callback);
const conversationAuth = a.schema({chat: a.conversation({aiModel: a.ai.model("Claude 3 Opus"), systemPrompt: ''}).authorization(callback)});
});
});
6 changes: 6 additions & 0 deletions packages/data-schema/__tests__/Authorization.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { expectTypeTestsToPassAsync } from 'jest-tsd';
import { a } from '../src/index';

// evaluates type defs in corresponding test-d.ts file
it('should not produce static type errors', async () => {
await expectTypeTestsToPassAsync(__filename);
});

describe('.authorization(allow) builder disallowed use cases', () => {
describe('allow.resource()', () => {
it('cannot be used with a.model()', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [@aws-amplify/data-schema](./data-schema.md) &gt; [a](./data-schema.a.md) &gt; [AuthorizationCallback](./data-schema.a.authorizationcallback.md)

## a.AuthorizationCallback type

Define an authorization callback that can be reused for multiple authorization calls across the schema definition.

**Signature:**

```typescript
export type AuthorizationCallback<AuthorizationType extends (keyof AuthorizationCallbackMapping) = "model"> = AuthorizationCallbackMapping[AuthorizationType];
```

## Example

const authCallback: a.AuthorizationCallback = (allow) =<!-- -->&gt; \[ allow.guest().to(\["read"\]), allow.owner() \];

const schema = a.schema(<!-- -->{ Post: a.model(<!-- -->{ id: a.id(), title: a.string(), protectedField: a.string().authorization(authCallback), content: a.string(), }<!-- -->).authorization(authCallback), }<!-- -->).authorization(authCallback);

26 changes: 26 additions & 0 deletions packages/data-schema/docs/data-schema.a.md
Original file line number Diff line number Diff line change
Expand Up @@ -355,5 +355,31 @@ Description
</td><td>


</td></tr>
</tbody></table>

## Type Aliases

<table><thead><tr><th>

Type Alias


</th><th>

Description


</th></tr></thead>
<tbody><tr><td>

[AuthorizationCallback](./data-schema.a.authorizationcallback.md)


</td><td>

Define an authorization callback that can be reused for multiple authorization calls across the schema definition.


</td></tr>
</tbody></table>
20 changes: 20 additions & 0 deletions packages/data-schema/docs/data-schema.authorizationcallback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [@aws-amplify/data-schema](./data-schema.md) &gt; [AuthorizationCallback](./data-schema.authorizationcallback.md)

## AuthorizationCallback type

Define an authorization callback that can be reused for multiple authorization calls across the schema definition.

**Signature:**

```typescript
export type AuthorizationCallback<AuthorizationType extends (keyof AuthorizationCallbackMapping) = "model"> = AuthorizationCallbackMapping[AuthorizationType];
```

## Example

const authCallback: a.AuthorizationCallback = (allow) =<!-- -->&gt; \[ allow.guest().to(\["read"\]), allow.owner() \];

const schema = a.schema(<!-- -->{ Post: a.model(<!-- -->{ id: a.id(), title: a.string(), protectedField: a.string().authorization(authCallback), content: a.string(), }<!-- -->).authorization(authCallback), }<!-- -->).authorization(authCallback);

2 changes: 1 addition & 1 deletion packages/data-schema/docs/data-schema.customoperation.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Custom operation definition interface
export type CustomOperation<T extends CustomOperationParamShape, K extends keyof CustomOperation<T> = never, B extends CustomOperationBrand = CustomOperationBrand> = Omit<{
arguments<Arguments extends CustomArguments>(args: Arguments): CustomOperation<SetTypeSubArg<T, 'arguments', Arguments>, K | 'arguments', B>;
returns<ReturnType extends CustomReturnType>(returnType: ReturnType): CustomOperation<SetTypeSubArg<T, 'returnType', ReturnType>, K | 'returns', B>;
authorization<AuthRuleType extends Authorization<any, any, any>>(callback: (allow: AllowModifierForCustomOperation) => AuthRuleType | AuthRuleType[]): CustomOperation<SetTypeSubArg<T, 'authorization', AuthRuleType[]>, K | 'authorization', B>;
authorization<AuthRuleType extends Authorization<any, any, any>>(callback: CustomOperationAuthorizationCallback<AuthRuleType>): CustomOperation<SetTypeSubArg<T, 'authorization', AuthRuleType[]>, K | 'authorization', B>;
handler<H extends HandlerInputType>(handlers: H): [H] extends [UltimateFunctionHandlerAsyncType] ? CustomOperation<AsyncFunctionCustomOperation<T>, K | 'handler' | 'returns', B> : CustomOperation<T, K | 'handler', B>;
for<Source extends SubscriptionSource>(source: Source | Source[]): CustomOperation<T['typeName'] extends 'Subscription' ? SetTypeSubArg<T, 'returnType', Source extends SubscriptionSource[] ? Source[number] : Source> : T, K | 'for', B>;
}, K> & Brand<B>;
Expand Down
11 changes: 11 additions & 0 deletions packages/data-schema/docs/data-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,17 @@ Description
Container for authorization schema definition content.


</td></tr>
<tr><td>

[AuthorizationCallback](./data-schema.authorizationcallback.md)


</td><td>

Define an authorization callback that can be reused for multiple authorization calls across the schema definition.


</td></tr>
<tr><td>

Expand Down
2 changes: 1 addition & 1 deletion packages/data-schema/docs/data-schema.modelschema.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Model schema definition interface

```typescript
export type ModelSchema<T extends ModelSchemaParamShape, UsedMethods extends 'authorization' | 'relationships' = never> = Omit<{
authorization: <AuthRules extends SchemaAuthorization<any, any, any>>(callback: (allow: AllowModifier) => AuthRules | AuthRules[]) => ModelSchema<SetTypeSubArg<T, 'authorization', AuthRules[]>, UsedMethods | 'authorization'>;
authorization: <AuthRules extends SchemaAuthorization<any, any, any>>(callback: SchemaAuthorizationCallback<AuthRules>) => ModelSchema<SetTypeSubArg<T, 'authorization', AuthRules[]>, UsedMethods | 'authorization'>;
}, UsedMethods> & BaseSchema<T> & DDBSchemaBrand;
```
**References:** [ModelSchema](./data-schema.modelschema.md)
Expand Down
2 changes: 1 addition & 1 deletion packages/data-schema/docs/data-schema.modeltype.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export type ModelType<T extends ModelTypeParamShape = ModelTypeParamShape, UsedM
identifier<PrimaryIndexFields = ExtractSecondaryIndexIRFields<T>, PrimaryIndexPool extends string = keyof PrimaryIndexFields & string, const ID extends ReadonlyArray<PrimaryIndexPool> = readonly [], const PrimaryIndexIR extends PrimaryIndexIrShape = PrimaryIndexFieldsToIR<ID, PrimaryIndexFields>>(identifier: ID): ModelType<SetTypeSubArg<T, 'identifier', PrimaryIndexIR>, UsedMethod | 'identifier'>;
secondaryIndexes<const SecondaryIndexFields = ExtractSecondaryIndexIRFields<T>, const SecondaryIndexPKPool extends string = keyof SecondaryIndexFields & string, const Indexes extends readonly ModelIndexType<string, string, unknown, readonly [], any>[] = readonly [], const IndexesIR extends readonly any[] = SecondaryIndexToIR<Indexes, SecondaryIndexFields>>(callback: (index: <PK extends SecondaryIndexPKPool>(pk: PK) => ModelIndexType<SecondaryIndexPKPool, PK, ReadonlyArray<Exclude<SecondaryIndexPKPool, PK>>>) => Indexes): ModelType<SetTypeSubArg<T, 'secondaryIndexes', IndexesIR>, UsedMethod | 'secondaryIndexes'>;
disableOperations<const Ops extends ReadonlyArray<DisableOperationsOptions>>(ops: Ops): ModelType<SetTypeSubArg<T, 'disabledOperations', Ops>, UsedMethod | 'disableOperations'>;
authorization<AuthRuleType extends AnyAuthorization>(callback: (allow: BaseAllowModifier) => AuthRuleType | AuthRuleType[]): ModelType<SetTypeSubArg<T, 'authorization', AuthRuleType[]>, UsedMethod | 'authorization'>;
authorization<AuthRuleType extends AnyAuthorization>(callback: ModelAuthorizationCallback<AuthRuleType>): ModelType<SetTypeSubArg<T, 'authorization', AuthRuleType[]>, UsedMethod | 'authorization'>;
}, UsedMethod>;
```
**References:** [ModelType](./data-schema.modeltype.md)
Expand Down
2 changes: 1 addition & 1 deletion packages/data-schema/docs/data-schema.rdsmodelschema.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export type RDSModelSchema<T extends RDSModelSchemaParamShape, UsedMethods exten
addQueries: <Queries extends Record<string, QueryCustomOperation>>(types: Queries) => RDSModelSchema<SetTypeSubArg<T, 'types', T['types'] & Queries>, UsedMethods | 'addQueries'>;
addMutations: <Mutations extends Record<string, MutationCustomOperation>>(types: Mutations) => RDSModelSchema<SetTypeSubArg<T, 'types', T['types'] & Mutations>, UsedMethods | 'addMutations'>;
addSubscriptions: <Subscriptions extends Record<string, SubscriptionCustomOperation>>(types: Subscriptions) => RDSModelSchema<SetTypeSubArg<T, 'types', T['types'] & Subscriptions>, UsedMethods | 'addSubscriptions'>;
authorization: <AuthRules extends SchemaAuthorization<any, any, any>>(callback: (allow: AllowModifier) => AuthRules | AuthRules[]) => RDSModelSchema<SetTypeSubArg<T, 'authorization', AuthRules[]>, UsedMethods | 'authorization'>;
authorization: <AuthRules extends SchemaAuthorization<any, any, any>>(callback: SchemaAuthorizationCallback<AuthRules>) => RDSModelSchema<SetTypeSubArg<T, 'authorization', AuthRules[]>, UsedMethods | 'authorization'>;
setAuthorization: (callback: (models: OmitFromEach<BaseSchema<T, true>['models'], 'secondaryIndexes'>, schema: RDSModelSchema<T, UsedMethods | 'setAuthorization'>) => void) => RDSModelSchema<T>;
setRelationships: <Relationships extends ReadonlyArray<Partial<Record<keyof T['types'], RelationshipTemplate>>>>(callback: (models: OmitFromEach<BaseSchema<T, true>['models'], 'authorization' | 'fields' | 'secondaryIndexes'>) => Relationships) => RDSModelSchema<SetTypeSubArg<T, 'types', {
[ModelName in keyof T['types']]: ModelWithRelationships<T['types'], Relationships, ModelName>;
Expand Down
Loading