Skip to content

Commit 956ace2

Browse files
committed
feat(datastore): add subscription variables for multi-tenant filtering
Add subscriptionVariables config option to DataStore.configure() that passes custom GraphQL variables to subscription operations. Enables server-side filtering for multi-tenant apps instead of client-side. Usage: DataStore.configure({ subscriptionVariables: { Todo: { storeId: 'store-123' }, Order: (op) => ({ storeId: op === 'DELETE' ? undefined : 'store-123' }), }, }); Variables are validated (must be plain objects), reserved GraphQL names (filter, owner, input, etc.) are filtered out with warnings, and function-form variables receive the operation type. Fixes #9413
1 parent 93487ff commit 956ace2

8 files changed

Lines changed: 605 additions & 0 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@aws-amplify/datastore': minor
3+
---
4+
5+
feat(datastore): add subscription variables support for multi-tenant filtering
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import { TransformerMutationType, processSubscriptionVariables } from '../src/sync/utils';
2+
import { InternalSchema } from '../src/types';
3+
4+
describe('Subscription Variables - Edge Cases & Safety', () => {
5+
beforeEach(() => {
6+
jest.clearAllMocks();
7+
});
8+
9+
const createTestSchema = (): InternalSchema => ({
10+
namespaces: {
11+
user: {
12+
name: 'user',
13+
models: {
14+
Todo: {
15+
name: 'Todo',
16+
pluralName: 'Todos',
17+
syncable: true,
18+
attributes: [],
19+
fields: {
20+
id: {
21+
name: 'id',
22+
type: 'ID',
23+
isRequired: true,
24+
isArray: false,
25+
},
26+
},
27+
},
28+
},
29+
relationships: {},
30+
enums: {},
31+
nonModels: {},
32+
},
33+
},
34+
version: '1',
35+
codegenVersion: '3.0.0',
36+
});
37+
38+
describe('Invalid Input Handling', () => {
39+
it('should reject non-object static variables', () => {
40+
const schema = createTestSchema();
41+
42+
const testCases = [
43+
{ value: 'string', desc: 'string' },
44+
{ value: 123, desc: 'number' },
45+
{ value: true, desc: 'boolean' },
46+
{ value: ['array'], desc: 'array' },
47+
];
48+
49+
testCases.forEach(({ value }) => {
50+
const result = processSubscriptionVariables(
51+
schema.namespaces.user.models.Todo,
52+
TransformerMutationType.CREATE,
53+
value as any,
54+
);
55+
56+
expect(result).toBeUndefined();
57+
});
58+
});
59+
60+
it('should handle Object.create(null) objects', () => {
61+
const schema = createTestSchema();
62+
const nullProtoObj = Object.create(null);
63+
nullProtoObj.storeId = 'test';
64+
65+
const result = processSubscriptionVariables(
66+
schema.namespaces.user.models.Todo,
67+
TransformerMutationType.CREATE,
68+
nullProtoObj,
69+
);
70+
71+
expect(result).toBeDefined();
72+
expect(result?.storeId).toBe('test');
73+
});
74+
75+
it('should handle function that throws', () => {
76+
const schema = createTestSchema();
77+
78+
const mockFn = () => {
79+
throw new Error('Function error');
80+
};
81+
const result = processSubscriptionVariables(
82+
schema.namespaces.user.models.Todo,
83+
TransformerMutationType.CREATE,
84+
mockFn,
85+
);
86+
87+
expect(result).toBeUndefined();
88+
});
89+
90+
it('should handle function returning non-object', () => {
91+
const schema = createTestSchema();
92+
93+
const testCases = [
94+
{ value: null, desc: 'null' },
95+
{ value: undefined, desc: 'undefined' },
96+
{ value: 'string', desc: 'string' },
97+
{ value: 123, desc: 'number' },
98+
{ value: ['array'], desc: 'array' },
99+
];
100+
101+
testCases.forEach(({ value }) => {
102+
const mockFn = () => value;
103+
const result = processSubscriptionVariables(
104+
schema.namespaces.user.models.Todo,
105+
TransformerMutationType.CREATE,
106+
mockFn as any,
107+
);
108+
109+
expect(result).toBeUndefined();
110+
});
111+
});
112+
});
113+
114+
describe('Reserved Variable Filtering', () => {
115+
it('should filter reserved names and keep custom ones', () => {
116+
const schema = createTestSchema();
117+
118+
const result = processSubscriptionVariables(
119+
schema.namespaces.user.models.Todo,
120+
TransformerMutationType.CREATE,
121+
{ storeId: 'store-1', filter: 'should-be-removed', owner: 'should-be-removed' },
122+
);
123+
124+
expect(result).toEqual({ storeId: 'store-1' });
125+
});
126+
127+
it('should return undefined when all variables are reserved', () => {
128+
const schema = createTestSchema();
129+
130+
const result = processSubscriptionVariables(
131+
schema.namespaces.user.models.Todo,
132+
TransformerMutationType.CREATE,
133+
{ filter: 'x', owner: 'y' },
134+
);
135+
136+
expect(result).toBeUndefined();
137+
});
138+
});
139+
140+
describe('Function Variables', () => {
141+
it('should call function with operation type', () => {
142+
const schema = createTestSchema();
143+
const mockFn = jest.fn(() => ({ storeId: 'test' }));
144+
145+
processSubscriptionVariables(
146+
schema.namespaces.user.models.Todo,
147+
TransformerMutationType.CREATE,
148+
mockFn,
149+
);
150+
151+
expect(mockFn).toHaveBeenCalledWith(TransformerMutationType.CREATE);
152+
});
153+
154+
it('should return different results per operation', () => {
155+
const schema = createTestSchema();
156+
const mockFn = jest.fn((op: string) => ({ op, storeId: 'test' }));
157+
158+
const createResult = processSubscriptionVariables(
159+
schema.namespaces.user.models.Todo,
160+
TransformerMutationType.CREATE,
161+
mockFn,
162+
);
163+
164+
const updateResult = processSubscriptionVariables(
165+
schema.namespaces.user.models.Todo,
166+
TransformerMutationType.UPDATE,
167+
mockFn,
168+
);
169+
170+
expect(createResult?.op).toBe(TransformerMutationType.CREATE);
171+
expect(updateResult?.op).toBe(TransformerMutationType.UPDATE);
172+
});
173+
});
174+
});

0 commit comments

Comments
 (0)