diff --git a/.changeset/fifty-parks-punch.md b/.changeset/fifty-parks-punch.md new file mode 100644 index 000000000..d11ab9f96 --- /dev/null +++ b/.changeset/fifty-parks-punch.md @@ -0,0 +1,5 @@ +--- +'@aws-amplify/data-schema': minor +--- + +Optimize custom selection set behavior diff --git a/.prettierignore b/.prettierignore index 6c9d9150d..89e1fe1de 100644 --- a/.prettierignore +++ b/.prettierignore @@ -5,4 +5,5 @@ bin lib docs tsconfig.tsbuildinfo -**/*.js \ No newline at end of file +**/*.js +packages/bench \ No newline at end of file diff --git a/packages/benches/combine/combine-CRUDL.bench.ts b/packages/benches/combine/combine-CRUDL.bench.ts index ade49af85..c2ca93736 100644 --- a/packages/benches/combine/combine-CRUDL.bench.ts +++ b/packages/benches/combine/combine-CRUDL.bench.ts @@ -110,4 +110,4 @@ bench('combined SQL and DDB schema w client types', async () => { title: 'updated', }); const _deletedBlog = await client.models.Blog.delete({ id: 'abc' }); -}).types([2295900, 'instantiations']); +}).types([2322525, 'instantiations']); diff --git a/packages/benches/p50/ai/p50-conversation-operations.bench.ts b/packages/benches/p50/ai/p50-conversation-operations.bench.ts index c54a13894..598261884 100644 --- a/packages/benches/p50/ai/p50-conversation-operations.bench.ts +++ b/packages/benches/p50/ai/p50-conversation-operations.bench.ts @@ -65,10 +65,8 @@ bench('p50 conversation operations', async () => { allow.publicApiKey().to(['read']), allow.owner(), ]), - ChatBot: a.conversation(input) - .authorization((allow) => allow.owner()), - GossipBot: a.conversation(input) - .authorization((allow) => allow.owner()), + ChatBot: a.conversation(input).authorization((allow) => allow.owner()), + GossipBot: a.conversation(input).authorization((allow) => allow.owner()), }) .authorization((allow) => allow.publicApiKey()); @@ -101,4 +99,4 @@ bench('p50 conversation operations', async () => { }); await conversation?.listMessages(); -}).types([13028, 'instantiations']); +}).types([20266, 'instantiations'][(20266, 'instantiations')]); diff --git a/packages/benches/p50/ai/p50-conversation-prod-operations.bench.ts b/packages/benches/p50/ai/p50-conversation-prod-operations.bench.ts index 3f34f7255..812c84947 100644 --- a/packages/benches/p50/ai/p50-conversation-prod-operations.bench.ts +++ b/packages/benches/p50/ai/p50-conversation-prod-operations.bench.ts @@ -607,16 +607,11 @@ bench('prod p50 conversation operations', async () => { allow.authenticated('identityPool').to(['read']), allow.owner(), ]), - ChatBot: a.conversation(input) - .authorization((allow) => allow.owner()), - GossipBot: a.conversation(input) - .authorization((allow) => allow.owner()), - HaikuBot: a.conversation(input) - .authorization((allow) => allow.owner()), - MathBot: a.conversation(input) - .authorization((allow) => allow.owner()), - ScienceBot: a.conversation(input) - .authorization((allow) => allow.owner()), + ChatBot: a.conversation(input).authorization((allow) => allow.owner()), + GossipBot: a.conversation(input).authorization((allow) => allow.owner()), + HaikuBot: a.conversation(input).authorization((allow) => allow.owner()), + MathBot: a.conversation(input).authorization((allow) => allow.owner()), + ScienceBot: a.conversation(input).authorization((allow) => allow.owner()), // [Global authorization rule] }) .authorization((allow) => allow.publicApiKey()); @@ -650,4 +645,4 @@ bench('prod p50 conversation operations', async () => { }); await conversation?.listMessages(); -}).types([35375, 'instantiations']); +}).types([44021, 'instantiations'][(44021, 'instantiations')]); diff --git a/packages/benches/p50/ai/p50-conversation-prod.bench.ts b/packages/benches/p50/ai/p50-conversation-prod.bench.ts index 8f3b75e2f..81ce22df8 100644 --- a/packages/benches/p50/ai/p50-conversation-prod.bench.ts +++ b/packages/benches/p50/ai/p50-conversation-prod.bench.ts @@ -615,7 +615,7 @@ bench('prod p50 conversation', () => { .authorization((allow) => allow.owner()), // [Global authorization rule] }).authorization((allow) => allow.publicApiKey()); -}).types([20837, 'instantiations']); +}).types([28904,"instantiations"]); bench('prod p50 conversation w/ client types', () => { const s = a @@ -1217,7 +1217,7 @@ bench('prod p50 conversation w/ client types', () => { .authorization((allow) => allow.publicApiKey()); type _ = ClientSchema; -}).types([22779, 'instantiations']); +}).types([31252,"instantiations"]); bench('prod p50 conversation combined w/ client types', () => { const s1 = a.schema({ @@ -1820,4 +1820,4 @@ bench('prod p50 conversation combined w/ client types', () => { const s = a.combine([s1, s2]); type _ = ClientSchema; -}).types([27012, 'instantiations']); +}).types([35642,"instantiations"]); diff --git a/packages/benches/p50/ai/p50-conversation.bench.ts b/packages/benches/p50/ai/p50-conversation.bench.ts index 7ce5f8a60..462a0041a 100644 --- a/packages/benches/p50/ai/p50-conversation.bench.ts +++ b/packages/benches/p50/ai/p50-conversation.bench.ts @@ -65,7 +65,7 @@ bench('p50 conversation', () => { GossipBot: a.conversation(input) .authorization((allow) => allow.owner()), }).authorization((allow) => allow.publicApiKey()); -}).types([8514, 'instantiations']); +}).types([15287,"instantiations"]); bench('p50 conversation w/ client types', () => { const s = a @@ -120,7 +120,7 @@ bench('p50 conversation w/ client types', () => { .authorization((allow) => allow.publicApiKey()); type _ = ClientSchema; -}).types([10441, 'instantiations']); +}).types([17622,"instantiations"]); bench('p50 combined conversation w/ client types', () => { const s1 = a @@ -183,4 +183,4 @@ bench('p50 combined conversation w/ client types', () => { const s = a.combine([s1, s2]); type _ = ClientSchema; -}).types([13892, 'instantiations']); +}).types([21309,"instantiations"]); diff --git a/packages/benches/p50/operations/p50-CRUDL.bench.ts b/packages/benches/p50/operations/p50-CRUDL.bench.ts index 7d0a2d9f3..1cd7cb179 100644 --- a/packages/benches/p50/operations/p50-CRUDL.bench.ts +++ b/packages/benches/p50/operations/p50-CRUDL.bench.ts @@ -92,4 +92,4 @@ bench('p50 CRUDL', async () => { }); await client.models.Todo.list(); -}).types([355529, 'instantiations']); +}).types([365796, 'instantiations']); diff --git a/packages/benches/p50/operations/p50-prod-CRUDL.bench.ts b/packages/benches/p50/operations/p50-prod-CRUDL.bench.ts index e0f2c9329..fca08aef1 100644 --- a/packages/benches/p50/operations/p50-prod-CRUDL.bench.ts +++ b/packages/benches/p50/operations/p50-prod-CRUDL.bench.ts @@ -629,4 +629,4 @@ bench('prod p50 CRUDL', async () => { }); await client.models.Todo.list(); -}).types([692058, 'instantiations']); +}).types([699671, 'instantiations']); diff --git a/packages/benches/p50/operations/p50-prod-selection-set.bench.ts b/packages/benches/p50/operations/p50-prod-selection-set.bench.ts index 70778a1aa..83b862197 100644 --- a/packages/benches/p50/operations/p50-prod-selection-set.bench.ts +++ b/packages/benches/p50/operations/p50-prod-selection-set.bench.ts @@ -632,4 +632,4 @@ bench('prod p50 CRUDL', async () => { }); await client.models.Todo.list({ selectionSet }); -}).types([715279, 'instantiations']); +}).types([722895, 'instantiations']); diff --git a/packages/benches/p50/operations/p50-selection-set.bench.ts b/packages/benches/p50/operations/p50-selection-set.bench.ts index fc0545e74..3dd3e3732 100644 --- a/packages/benches/p50/operations/p50-selection-set.bench.ts +++ b/packages/benches/p50/operations/p50-selection-set.bench.ts @@ -94,4 +94,4 @@ bench('p50 CRUDL', async () => { }); await client.models.Todo.list({ selectionSet }); -}).types([378409, 'instantiations']); +}).types([388679, 'instantiations']); diff --git a/packages/benches/p50/p50-prod.bench.ts b/packages/benches/p50/p50-prod.bench.ts index fc25415a1..84c90d9f3 100644 --- a/packages/benches/p50/p50-prod.bench.ts +++ b/packages/benches/p50/p50-prod.bench.ts @@ -596,7 +596,7 @@ bench('prod p50', () => { ]), // [Global authorization rule] }).authorization((allow) => allow.publicApiKey()); -}).types([20946, 'instantiations']); +}).types([28900, 'instantiations'][(28900, 'instantiations')]); bench('prod p50 w/ client types', () => { const s = a @@ -1188,7 +1188,7 @@ bench('prod p50 w/ client types', () => { .authorization((allow) => allow.publicApiKey()); type _ = ClientSchema; -}).types([23327, 'instantiations']); +}).types([31248, 'instantiations'][(31248, 'instantiations')]); bench('prod p50 combined w/ client types', () => { const s1 = a.schema({ @@ -1781,4 +1781,4 @@ bench('prod p50 combined w/ client types', () => { const s = a.combine([s1, s2]); type _ = ClientSchema; -}).types([27507, 'instantiations']); +}).types([35583, 'instantiations'][(35583, 'instantiations')]); diff --git a/packages/benches/p50/p50.bench.ts b/packages/benches/p50/p50.bench.ts index 1d3674aa6..e12edf1db 100644 --- a/packages/benches/p50/p50.bench.ts +++ b/packages/benches/p50/p50.bench.ts @@ -51,7 +51,7 @@ bench('p50', () => { allow.owner(), ]), }).authorization((allow) => allow.publicApiKey()); -}).types([8572, 'instantiations']); +}).types([15283, 'instantiations'][(15283, 'instantiations')]); bench('p50 w/ client types', () => { const s = a @@ -102,7 +102,7 @@ bench('p50 w/ client types', () => { .authorization((allow) => allow.publicApiKey()); type _ = ClientSchema; -}).types([10938, 'instantiations']); +}).types([17618, 'instantiations'][(17618, 'instantiations')]); bench('p50 combined schema w/ client types', () => { const s1 = a @@ -157,4 +157,4 @@ bench('p50 combined schema w/ client types', () => { const s = a.combine([s1, s2]); type _ = ClientSchema; -}).types([14350, 'instantiations']); +}).types([21264, 'instantiations'][(21264, 'instantiations')]); diff --git a/packages/benches/p99/over-limit/operations/p99-complex-relationships.bench.ts b/packages/benches/p99/over-limit/operations/p99-complex-relationships.bench.ts index c0162eb3d..736236b46 100644 --- a/packages/benches/p99/over-limit/operations/p99-complex-relationships.bench.ts +++ b/packages/benches/p99/over-limit/operations/p99-complex-relationships.bench.ts @@ -254,4 +254,4 @@ bench('complex relationships real world CRUDL', async () => { // console.log(items[0].id); // }, // }); -}).types([41538, 'instantiations']); +}).types([49464, 'instantiations']); diff --git a/packages/benches/p99/within-limit/ai/p99-conversation.bench.ts b/packages/benches/p99/within-limit/ai/p99-conversation.bench.ts index c3c0880a3..3a1cb1c30 100644 --- a/packages/benches/p99/within-limit/ai/p99-conversation.bench.ts +++ b/packages/benches/p99/within-limit/ai/p99-conversation.bench.ts @@ -16,414 +16,614 @@ const input = { bench('100 conversations', () => { a.schema({ - Conversation0: a.conversation(input) + Conversation0: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation1: a.conversation(input) + Conversation1: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation2: a.conversation(input) + Conversation2: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation3: a.conversation(input) + Conversation3: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation4: a.conversation(input) + Conversation4: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation5: a.conversation(input) + Conversation5: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation6: a.conversation(input) + Conversation6: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation7: a.conversation(input) + Conversation7: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation8: a.conversation(input) + Conversation8: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation9: a.conversation(input) + Conversation9: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation10: a.conversation(input) + Conversation10: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation11: a.conversation(input) + Conversation11: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation12: a.conversation(input) + Conversation12: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation13: a.conversation(input) + Conversation13: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation14: a.conversation(input) + Conversation14: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation15: a.conversation(input) + Conversation15: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation16: a.conversation(input) + Conversation16: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation17: a.conversation(input) + Conversation17: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation18: a.conversation(input) + Conversation18: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation19: a.conversation(input) + Conversation19: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation20: a.conversation(input) + Conversation20: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation21: a.conversation(input) + Conversation21: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation22: a.conversation(input) + Conversation22: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation23: a.conversation(input) + Conversation23: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation24: a.conversation(input) + Conversation24: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation25: a.conversation(input) + Conversation25: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation26: a.conversation(input) + Conversation26: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation27: a.conversation(input) + Conversation27: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation28: a.conversation(input) + Conversation28: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation29: a.conversation(input) + Conversation29: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation30: a.conversation(input) + Conversation30: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation31: a.conversation(input) + Conversation31: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation32: a.conversation(input) + Conversation32: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation33: a.conversation(input) + Conversation33: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation34: a.conversation(input) + Conversation34: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation35: a.conversation(input) + Conversation35: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation36: a.conversation(input) + Conversation36: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation37: a.conversation(input) + Conversation37: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation38: a.conversation(input) + Conversation38: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation39: a.conversation(input) + Conversation39: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation40: a.conversation(input) + Conversation40: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation41: a.conversation(input) + Conversation41: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation42: a.conversation(input) + Conversation42: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation43: a.conversation(input) + Conversation43: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation44: a.conversation(input) + Conversation44: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation45: a.conversation(input) + Conversation45: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation46: a.conversation(input) + Conversation46: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation47: a.conversation(input) + Conversation47: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation48: a.conversation(input) + Conversation48: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation49: a.conversation(input) + Conversation49: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation50: a.conversation(input) + Conversation50: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation51: a.conversation(input) + Conversation51: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation52: a.conversation(input) + Conversation52: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation53: a.conversation(input) + Conversation53: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation54: a.conversation(input) + Conversation54: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation55: a.conversation(input) + Conversation55: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation56: a.conversation(input) + Conversation56: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation57: a.conversation(input) + Conversation57: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation58: a.conversation(input) + Conversation58: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation59: a.conversation(input) + Conversation59: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation60: a.conversation(input) + Conversation60: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation61: a.conversation(input) + Conversation61: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation62: a.conversation(input) + Conversation62: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation63: a.conversation(input) + Conversation63: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation64: a.conversation(input) + Conversation64: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation65: a.conversation(input) + Conversation65: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation66: a.conversation(input) + Conversation66: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation67: a.conversation(input) + Conversation67: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation68: a.conversation(input) + Conversation68: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation69: a.conversation(input) + Conversation69: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation70: a.conversation(input) + Conversation70: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation71: a.conversation(input) + Conversation71: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation72: a.conversation(input) + Conversation72: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation73: a.conversation(input) + Conversation73: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation74: a.conversation(input) + Conversation74: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation75: a.conversation(input) + Conversation75: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation76: a.conversation(input) + Conversation76: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation77: a.conversation(input) + Conversation77: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation78: a.conversation(input) + Conversation78: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation79: a.conversation(input) + Conversation79: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation80: a.conversation(input) + Conversation80: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation81: a.conversation(input) + Conversation81: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation82: a.conversation(input) + Conversation82: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation83: a.conversation(input) + Conversation83: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation84: a.conversation(input) + Conversation84: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation85: a.conversation(input) + Conversation85: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation86: a.conversation(input) + Conversation86: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation87: a.conversation(input) + Conversation87: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation88: a.conversation(input) + Conversation88: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation89: a.conversation(input) + Conversation89: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation90: a.conversation(input) + Conversation90: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation91: a.conversation(input) + Conversation91: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation92: a.conversation(input) + Conversation92: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation93: a.conversation(input) + Conversation93: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation94: a.conversation(input) + Conversation94: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation95: a.conversation(input) + Conversation95: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation96: a.conversation(input) + Conversation96: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation97: a.conversation(input) + Conversation97: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation98: a.conversation(input) + Conversation98: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation99: a.conversation(input) + Conversation99: a + .conversation(input) .authorization((allow) => allow.owner()), }).authorization((allow) => allow.publicApiKey()); -}).types([1070, 'instantiations']); +}).types([1209, 'instantiations']); bench('100 conversations w/ client types', () => { const s = a .schema({ - Conversation0: a.conversation(input) + Conversation0: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation1: a.conversation(input) + Conversation1: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation2: a.conversation(input) + Conversation2: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation3: a.conversation(input) + Conversation3: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation4: a.conversation(input) + Conversation4: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation5: a.conversation(input) + Conversation5: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation6: a.conversation(input) + Conversation6: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation7: a.conversation(input) + Conversation7: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation8: a.conversation(input) + Conversation8: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation9: a.conversation(input) + Conversation9: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation10: a.conversation(input) + Conversation10: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation11: a.conversation(input) + Conversation11: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation12: a.conversation(input) + Conversation12: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation13: a.conversation(input) + Conversation13: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation14: a.conversation(input) + Conversation14: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation15: a.conversation(input) + Conversation15: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation16: a.conversation(input) + Conversation16: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation17: a.conversation(input) + Conversation17: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation18: a.conversation(input) + Conversation18: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation19: a.conversation(input) + Conversation19: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation20: a.conversation(input) + Conversation20: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation21: a.conversation(input) + Conversation21: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation22: a.conversation(input) + Conversation22: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation23: a.conversation(input) + Conversation23: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation24: a.conversation(input) + Conversation24: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation25: a.conversation(input) + Conversation25: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation26: a.conversation(input) + Conversation26: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation27: a.conversation(input) + Conversation27: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation28: a.conversation(input) + Conversation28: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation29: a.conversation(input) + Conversation29: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation30: a.conversation(input) + Conversation30: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation31: a.conversation(input) + Conversation31: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation32: a.conversation(input) + Conversation32: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation33: a.conversation(input) + Conversation33: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation34: a.conversation(input) + Conversation34: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation35: a.conversation(input) + Conversation35: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation36: a.conversation(input) + Conversation36: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation37: a.conversation(input) + Conversation37: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation38: a.conversation(input) + Conversation38: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation39: a.conversation(input) + Conversation39: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation40: a.conversation(input) + Conversation40: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation41: a.conversation(input) + Conversation41: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation42: a.conversation(input) + Conversation42: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation43: a.conversation(input) + Conversation43: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation44: a.conversation(input) + Conversation44: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation45: a.conversation(input) + Conversation45: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation46: a.conversation(input) + Conversation46: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation47: a.conversation(input) + Conversation47: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation48: a.conversation(input) + Conversation48: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation49: a.conversation(input) + Conversation49: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation50: a.conversation(input) + Conversation50: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation51: a.conversation(input) + Conversation51: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation52: a.conversation(input) + Conversation52: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation53: a.conversation(input) + Conversation53: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation54: a.conversation(input) + Conversation54: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation55: a.conversation(input) + Conversation55: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation56: a.conversation(input) + Conversation56: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation57: a.conversation(input) + Conversation57: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation58: a.conversation(input) + Conversation58: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation59: a.conversation(input) + Conversation59: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation60: a.conversation(input) + Conversation60: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation61: a.conversation(input) + Conversation61: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation62: a.conversation(input) + Conversation62: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation63: a.conversation(input) + Conversation63: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation64: a.conversation(input) + Conversation64: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation65: a.conversation(input) + Conversation65: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation66: a.conversation(input) + Conversation66: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation67: a.conversation(input) + Conversation67: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation68: a.conversation(input) + Conversation68: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation69: a.conversation(input) + Conversation69: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation70: a.conversation(input) + Conversation70: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation71: a.conversation(input) + Conversation71: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation72: a.conversation(input) + Conversation72: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation73: a.conversation(input) + Conversation73: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation74: a.conversation(input) + Conversation74: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation75: a.conversation(input) + Conversation75: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation76: a.conversation(input) + Conversation76: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation77: a.conversation(input) + Conversation77: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation78: a.conversation(input) + Conversation78: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation79: a.conversation(input) + Conversation79: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation80: a.conversation(input) + Conversation80: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation81: a.conversation(input) + Conversation81: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation82: a.conversation(input) + Conversation82: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation83: a.conversation(input) + Conversation83: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation84: a.conversation(input) + Conversation84: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation85: a.conversation(input) + Conversation85: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation86: a.conversation(input) + Conversation86: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation87: a.conversation(input) + Conversation87: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation88: a.conversation(input) + Conversation88: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation89: a.conversation(input) + Conversation89: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation90: a.conversation(input) + Conversation90: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation91: a.conversation(input) + Conversation91: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation92: a.conversation(input) + Conversation92: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation93: a.conversation(input) + Conversation93: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation94: a.conversation(input) + Conversation94: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation95: a.conversation(input) + Conversation95: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation96: a.conversation(input) + Conversation96: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation97: a.conversation(input) + Conversation97: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation98: a.conversation(input) + Conversation98: a + .conversation(input) .authorization((allow) => allow.owner()), - Conversation99: a.conversation(input) + Conversation99: a + .conversation(input) .authorization((allow) => allow.owner()), }) .authorization((allow) => allow.publicApiKey()); type _ = ClientSchema; -}).types([3159, 'instantiations']); +}).types([3734, 'instantiations']); diff --git a/packages/benches/p99/within-limit/operations/p99-complex-sql-CRUDL.bench.ts b/packages/benches/p99/within-limit/operations/p99-complex-sql-CRUDL.bench.ts index ab851179e..c38e2d337 100644 --- a/packages/benches/p99/within-limit/operations/p99-complex-sql-CRUDL.bench.ts +++ b/packages/benches/p99/within-limit/operations/p99-complex-sql-CRUDL.bench.ts @@ -377,4 +377,4 @@ bench('complex SQL', async () => { const { data: _listedAssignments } = await client.models.Assignment.list(); const { data: _lazyLoadedContract } = await createdAssignment!.contract(); -}).types([8107908, 'instantiations']); +}).types([8125937, 'instantiations']); diff --git a/packages/benches/p99/within-limit/operations/p99-tall-complex-CRUDL.bench.ts b/packages/benches/p99/within-limit/operations/p99-tall-complex-CRUDL.bench.ts index f6bb7a94d..df86c7b27 100644 --- a/packages/benches/p99/within-limit/operations/p99-tall-complex-CRUDL.bench.ts +++ b/packages/benches/p99/within-limit/operations/p99-tall-complex-CRUDL.bench.ts @@ -2619,4 +2619,4 @@ bench('99 complex models CRUDL', async () => { }); await client.models.FieldLevelAuthModel1.list(); -}).types([503326, 'instantiations']); +}).types([513818, 'instantiations']); diff --git a/packages/benches/p99/within-limit/operations/p99-tall-simple-CRUDL.bench.ts b/packages/benches/p99/within-limit/operations/p99-tall-simple-CRUDL.bench.ts index 9f5f90874..0e6829ce5 100644 --- a/packages/benches/p99/within-limit/operations/p99-tall-simple-CRUDL.bench.ts +++ b/packages/benches/p99/within-limit/operations/p99-tall-simple-CRUDL.bench.ts @@ -250,4 +250,4 @@ bench('70 simple models with 1 field each w/ client types', async () => { await client.models.Model1.delete({ id: result.data!.id }); await client.models.Model1.list(); -}).types([168777, 'instantiations']); +}).types([176527, 'instantiations']); diff --git a/packages/benches/p99/within-limit/operations/p99-tall-simple-selection-set.bench.ts b/packages/benches/p99/within-limit/operations/p99-tall-simple-selection-set.bench.ts index ad0b5bdc6..628cb06a2 100644 --- a/packages/benches/p99/within-limit/operations/p99-tall-simple-selection-set.bench.ts +++ b/packages/benches/p99/within-limit/operations/p99-tall-simple-selection-set.bench.ts @@ -252,4 +252,4 @@ bench('70 simple models with 1 field each w/ client types', async () => { await client.models.Model1.delete({ id: result.data!.id }); await client.models.Model1.list({ selectionSet }); -}).types([189520, 'instantiations']); +}).types([197269, 'instantiations']); diff --git a/packages/benches/p99/within-limit/operations/p99-very-tall-simple-CRUDL.bench.ts b/packages/benches/p99/within-limit/operations/p99-very-tall-simple-CRUDL.bench.ts index 95bef803a..4e3291140 100644 --- a/packages/benches/p99/within-limit/operations/p99-very-tall-simple-CRUDL.bench.ts +++ b/packages/benches/p99/within-limit/operations/p99-very-tall-simple-CRUDL.bench.ts @@ -4610,4 +4610,4 @@ bench('1522 simple models with 1 field each CRUDL', async () => { await client.models.Model1.delete({ id: result.data!.id }); await client.models.Model1.list(); -}).types([725036, 'instantiations']); +}).types([732643, 'instantiations']); diff --git a/packages/benches/p99/within-limit/operations/p99-very-wide-large-CRUDL.bench.ts b/packages/benches/p99/within-limit/operations/p99-very-wide-large-CRUDL.bench.ts index 73a62b753..2b56eacf0 100644 --- a/packages/benches/p99/within-limit/operations/p99-very-wide-large-CRUDL.bench.ts +++ b/packages/benches/p99/within-limit/operations/p99-very-wide-large-CRUDL.bench.ts @@ -10153,4 +10153,4 @@ bench( await client.models.Model35.list(); }, -).types([3653038, 'instantiations']); +).types([3668984, 'instantiations']); diff --git a/packages/benches/p99/within-limit/operations/p99-wide-small-CRUDL.bench.ts b/packages/benches/p99/within-limit/operations/p99-wide-small-CRUDL.bench.ts index 218d77b51..74871e0e7 100644 --- a/packages/benches/p99/within-limit/operations/p99-wide-small-CRUDL.bench.ts +++ b/packages/benches/p99/within-limit/operations/p99-wide-small-CRUDL.bench.ts @@ -91,4 +91,4 @@ bench('1 simple model w/ 43 fields CRUDL', async () => { await client.models.Model1.delete({ id: result.data!.id }); await client.models.Model1.list(); -}).types([838113, 'instantiations']); +}).types([852955, 'instantiations']); diff --git a/packages/benches/p99/within-limit/operations/p99-wide-small-selection-set.bench.ts b/packages/benches/p99/within-limit/operations/p99-wide-small-selection-set.bench.ts index 36b5deae6..ae8c558b1 100644 --- a/packages/benches/p99/within-limit/operations/p99-wide-small-selection-set.bench.ts +++ b/packages/benches/p99/within-limit/operations/p99-wide-small-selection-set.bench.ts @@ -93,4 +93,4 @@ bench('1 simple model w/ 43 fields CRUDL', async () => { await client.models.Model1.delete({ id: result.data!.id }); await client.models.Model1.list({ selectionSet }); -}).types([892328, 'instantiations']); +}).types([907200, 'instantiations']); diff --git a/packages/benches/p99/within-limit/p99-complex-sql.bench.ts b/packages/benches/p99/within-limit/p99-complex-sql.bench.ts index 47994df5c..78f1214f1 100644 --- a/packages/benches/p99/within-limit/p99-complex-sql.bench.ts +++ b/packages/benches/p99/within-limit/p99-complex-sql.bench.ts @@ -345,4 +345,4 @@ bench('complex SQL', async () => { ]); type _Schema = ClientSchema; -}).types([42593, 'instantiations']); +}).types([49357, 'instantiations']); diff --git a/packages/benches/p99/within-limit/p99-tall-simple.bench.ts b/packages/benches/p99/within-limit/p99-tall-simple.bench.ts index 731930959..3477cadd0 100644 --- a/packages/benches/p99/within-limit/p99-tall-simple.bench.ts +++ b/packages/benches/p99/within-limit/p99-tall-simple.bench.ts @@ -237,7 +237,7 @@ bench('100 simple models with 1 field each', () => { field1: a.string(), }), }).authorization((allow) => allow.publicApiKey()); -}).types([4077, 'instantiations']); +}).types([11142, 'instantiations']); bench('100 simple models with 1 field each w/ client types', () => { const s = a @@ -471,4 +471,4 @@ bench('100 simple models with 1 field each w/ client types', () => { .authorization((allow) => allow.publicApiKey()); type _ = ClientSchema; -}).types([6548, 'instantiations']); +}).types([13531, 'instantiations']); diff --git a/packages/benches/p99/within-limit/p99-very-tall-simple.bench.ts b/packages/benches/p99/within-limit/p99-very-tall-simple.bench.ts index aaf9e091f..0f44c5dab 100644 --- a/packages/benches/p99/within-limit/p99-very-tall-simple.bench.ts +++ b/packages/benches/p99/within-limit/p99-very-tall-simple.bench.ts @@ -4574,7 +4574,7 @@ bench('1522 simple models with 1 field each', () => { field1: a.string(), }), }).authorization((allow) => allow.publicApiKey()); -}).types([25782, 'instantiations']); +}).types([35741, 'instantiations']); bench('1522 simple models with 1 field each w/ client types', () => { const s = a @@ -9149,4 +9149,4 @@ bench('1522 simple models with 1 field each w/ client types', () => { .authorization((allow) => allow.publicApiKey()); type _ = ClientSchema; -}).types([28253, 'instantiations']); +}).types([38130, 'instantiations']); diff --git a/packages/benches/p99/within-limit/p99-very-wide-large.bench.ts b/packages/benches/p99/within-limit/p99-very-wide-large.bench.ts index 17f76b70b..7fe95869c 100644 --- a/packages/benches/p99/within-limit/p99-very-wide-large.bench.ts +++ b/packages/benches/p99/within-limit/p99-very-wide-large.bench.ts @@ -10110,7 +10110,7 @@ bench('1 model containing 2288 fields, 34 models w/ 215 fields each', () => { field215: a.string(), }), }).authorization((allow) => allow.publicApiKey()); -}).types([3507, 'instantiations']); +}).types([10496, 'instantiations']); bench( '1 model containing 2288 fields, 34 models w/ 215 fields each w/ client types', @@ -20224,4 +20224,4 @@ bench( type _ = ClientSchema; }, -).types([5978, 'instantiations']); +).types([12885, 'instantiations']); diff --git a/packages/benches/p99/within-limit/p99-wide-large.bench.ts b/packages/benches/p99/within-limit/p99-wide-large.bench.ts index cc9757c62..ac60d6892 100644 --- a/packages/benches/p99/within-limit/p99-wide-large.bench.ts +++ b/packages/benches/p99/within-limit/p99-wide-large.bench.ts @@ -5660,7 +5660,7 @@ bench('26 models w/ 215 fields each, 1 model with 4', () => { field4: a.string(), }), }).authorization((allow) => allow.publicApiKey()); -}).types([3357, 'instantiations']); +}).types([10326, 'instantiations']); bench('26 models w/ 215 fields each, 1 model with 4 w/ client types', () => { const s = a @@ -11317,4 +11317,4 @@ bench('26 models w/ 215 fields each, 1 model with 4 w/ client types', () => { .authorization((allow) => allow.publicApiKey()); type _ = ClientSchema; -}).types([5828, 'instantiations']); +}).types([12715, 'instantiations']); diff --git a/packages/benches/p99/within-limit/p99-wide-small.bench.ts b/packages/benches/p99/within-limit/p99-wide-small.bench.ts index 99c3d7718..a28d12986 100644 --- a/packages/benches/p99/within-limit/p99-wide-small.bench.ts +++ b/packages/benches/p99/within-limit/p99-wide-small.bench.ts @@ -1709,7 +1709,7 @@ bench('1 simple model w/ 1700 fields each', () => { field1700: a.string(), }), }).authorization((allow) => allow.publicApiKey()); -}).types([2967, 'instantiations']); +}).types([9884, 'instantiations']); bench('1 simple model w/ 1700 fields each w/ client types', () => { const s = a @@ -3420,4 +3420,4 @@ bench('1 simple model w/ 1700 fields each w/ client types', () => { .authorization((allow) => allow.publicApiKey()); type _ = ClientSchema; -}).types([5438, 'instantiations']); +}).types([12273, 'instantiations']); diff --git a/packages/data-schema/__tests__/FlatModel.test-d.ts b/packages/data-schema/__tests__/FlatModel.test-d.ts new file mode 100644 index 000000000..20f9f302f --- /dev/null +++ b/packages/data-schema/__tests__/FlatModel.test-d.ts @@ -0,0 +1,560 @@ +/** + * Property-based type tests for FlatModel generation and bi-directional relationship short-circuiting. + * + * These tests verify that the FlatModel type infrastructure correctly: + * 1. Short-circuits bi-directional relationships to prevent cyclical paths + * 2. Preserves non-cyclical relationship paths + * 3. Handles self-referential relationships correctly + */ +import type { + Equal, + Expect, + ExpectFalse, + HasKey, +} from '@aws-amplify/data-schema-types'; +import { a, ClientSchema } from '../src/index'; +import type { + FlatResolveFields, + ShortCircuitBiDirectionalRelationship, +} from '../src/ClientSchema/utilities'; +import type { ModelRelationshipField } from '../src/ModelRelationshipField'; + +/** + * **Feature: selection-set-optimization, Property 1: Bi-directional relationship short-circuiting** + * **Validates: Requirements 1.1** + * + * For any schema with a bi-directional relationship (parent with hasMany/hasOne to child, + * child with belongsTo to parent), the generated FlatModel for the parent SHALL NOT include + * the belongsTo field on the child model that references back to the parent. + */ +describe('Property 1: Bi-directional relationship short-circuiting', () => { + describe('hasMany/belongsTo bi-directional relationship', () => { + // Schema with Post (hasMany) -> Comment (belongsTo) -> Post + const schema = a.schema({ + Post: a.model({ + title: a.string(), + comments: a.hasMany('Comment', 'postId'), + }), + Comment: a.model({ + content: a.string(), + postId: a.id(), + post: a.belongsTo('Post', 'postId'), + }), + }); + + type Schema = ClientSchema; + + test('ShortCircuitBiDirectionalRelationship omits belongsTo field referencing parent', () => { + // Define raw fields that simulate Comment's field metadata + type CommentRawFields = { + content: any; + postId: any; + post: ModelRelationshipField< + { + type: 'model'; + relationshipType: 'belongsTo'; + relatedModel: 'Post'; + array: false; + valueRequired: false; + references: string[]; + arrayRequired: false; + }, + 'Post', + never, + undefined + >; + }; + + // Resolved Comment model fields (before short-circuiting) + type CommentFields = { + content: string | null; + postId: string | null; + post: any; // This would be the resolved relationship + }; + + // Apply short-circuit with 'Post' as parent + type ShortCircuitedComment = ShortCircuitBiDirectionalRelationship< + CommentFields, + 'Post', + CommentRawFields + >; + + // The 'post' field should be omitted because it's a belongsTo referencing 'Post' + type HasPostField = 'post' extends keyof ShortCircuitedComment + ? true + : false; + type test1 = Expect>; + + // Other fields should be preserved + type HasContentField = 'content' extends keyof ShortCircuitedComment + ? true + : false; + type test2 = Expect>; + + type HasPostIdField = 'postId' extends keyof ShortCircuitedComment + ? true + : false; + type test3 = Expect>; + }); + + test('ShortCircuitBiDirectionalRelationship preserves fields when no parent match', () => { + // Define raw fields that simulate Comment's field metadata + type CommentRawFields = { + content: any; + postId: any; + post: ModelRelationshipField< + { + type: 'model'; + relationshipType: 'belongsTo'; + relatedModel: 'Post'; + array: false; + valueRequired: false; + references: string[]; + arrayRequired: false; + }, + 'Post', + never, + undefined + >; + }; + + type CommentFields = { + content: string | null; + postId: string | null; + post: any; + }; + + // Apply short-circuit with 'Author' as parent (not 'Post') + // The 'post' field should NOT be omitted because it references 'Post', not 'Author' + type ShortCircuitedComment = ShortCircuitBiDirectionalRelationship< + CommentFields, + 'Author', + CommentRawFields + >; + + type HasPostField = 'post' extends keyof ShortCircuitedComment + ? true + : false; + type test = Expect>; + }); + }); + + describe('hasOne/belongsTo bi-directional relationship', () => { + test('ShortCircuitBiDirectionalRelationship omits belongsTo in hasOne relationship', () => { + // Schema: Customer (hasOne) -> Cart (belongsTo) -> Customer + type CartRawFields = { + items: any; + customerId: any; + customer: ModelRelationshipField< + { + type: 'model'; + relationshipType: 'belongsTo'; + relatedModel: 'Customer'; + array: false; + valueRequired: false; + references: string[]; + arrayRequired: false; + }, + 'Customer', + never, + undefined + >; + }; + + type CartFields = { + items: string[] | null; + customerId: string | null; + customer: any; + }; + + type ShortCircuitedCart = ShortCircuitBiDirectionalRelationship< + CartFields, + 'Customer', + CartRawFields + >; + + // The 'customer' field should be omitted + type HasCustomerField = 'customer' extends keyof ShortCircuitedCart + ? true + : false; + type test = Expect>; + }); + }); + + describe('non-belongsTo relationships are preserved', () => { + test('hasMany relationships are not short-circuited', () => { + type ParentRawFields = { + name: any; + children: ModelRelationshipField< + { + type: 'model'; + relationshipType: 'hasMany'; + relatedModel: 'Child'; + array: true; + valueRequired: false; + references: string[]; + arrayRequired: false; + }, + 'Child', + never, + undefined + >; + }; + + type ParentFields = { + name: string | null; + children: any[]; + }; + + type ShortCircuitedParent = ShortCircuitBiDirectionalRelationship< + ParentFields, + 'Child', + ParentRawFields + >; + + // hasMany should NOT be short-circuited even if it references the "parent" + type HasChildrenField = 'children' extends keyof ShortCircuitedParent + ? true + : false; + type test = Expect>; + }); + + test('hasOne relationships are not short-circuited', () => { + type ParentRawFields = { + name: any; + profile: ModelRelationshipField< + { + type: 'model'; + relationshipType: 'hasOne'; + relatedModel: 'Profile'; + array: false; + valueRequired: false; + references: string[]; + arrayRequired: false; + }, + 'Profile', + never, + undefined + >; + }; + + type ParentFields = { + name: string | null; + profile: any; + }; + + type ShortCircuitedParent = ShortCircuitBiDirectionalRelationship< + ParentFields, + 'Profile', + ParentRawFields + >; + + // hasOne should NOT be short-circuited + type HasProfileField = 'profile' extends keyof ShortCircuitedParent + ? true + : false; + type test = Expect>; + }); + }); + + describe('no parent model name (never) preserves all fields', () => { + test('when ParentModelName is never, all fields are preserved', () => { + type CommentRawFields = { + content: any; + post: ModelRelationshipField< + { + type: 'model'; + relationshipType: 'belongsTo'; + relatedModel: 'Post'; + array: false; + valueRequired: false; + references: string[]; + arrayRequired: false; + }, + 'Post', + never, + undefined + >; + }; + + type CommentFields = { + content: string | null; + post: any; + }; + + // When ParentModelName is never, no short-circuiting should occur + type ShortCircuitedComment = ShortCircuitBiDirectionalRelationship< + CommentFields, + never, + CommentRawFields + >; + + type HasPostField = 'post' extends keyof ShortCircuitedComment + ? true + : false; + type test = Expect>; + }); + }); +}); + +/** + * **Feature: selection-set-optimization, Property 2: Non-cyclical path preservation** + * **Validates: Requirements 2.1, 2.2, 2.3** + * + * For any schema with relationships, and for any non-cyclical path through those + * relationships up to 6 levels deep, the generated FlatModel type SHALL include + * that path as a valid selection set option. + */ +describe('Property 2: Non-cyclical path preservation', () => { + describe('non-cyclical paths through different models are preserved', () => { + // Schema: Author -> Post -> Comment (no cycles back to Author) + const schema = a.schema({ + Author: a.model({ + name: a.string().required(), + posts: a.hasMany('Post', 'authorId'), + }), + Post: a.model({ + title: a.string(), + authorId: a.id(), + author: a.belongsTo('Author', 'authorId'), + comments: a.hasMany('Comment', 'postId'), + }), + Comment: a.model({ + content: a.string(), + postId: a.id(), + post: a.belongsTo('Post', 'postId'), + }), + }); + + type Schema = ClientSchema; + + test('FlatModel preserves hasMany relationship paths', () => { + // Author's flatModel should include 'posts' field + type AuthorFlatModel = Schema['Author']['__meta']['flatModel']; + type HasPostsField = 'posts' extends keyof AuthorFlatModel ? true : false; + type test = Expect>; + }); + + test('FlatModel preserves nested non-cyclical paths (Author -> Post -> Comment)', () => { + // Author's flatModel should have posts, and posts should have comments + type AuthorFlatModel = Schema['Author']['__meta']['flatModel']; + type PostsType = AuthorFlatModel['posts']; + + // Posts should be an array + type PostsIsArray = PostsType extends Array ? true : false; + type test1 = Expect>; + + // Each post should have comments field (non-cyclical path) + type PostElement = PostsType extends Array ? P : never; + type HasCommentsField = 'comments' extends keyof PostElement + ? true + : false; + type test2 = Expect>; + }); + + test('FlatModel omits cyclical belongsTo but preserves non-cyclical belongsTo', () => { + // Post's flatModel should NOT have 'author' (cyclical back to parent when accessed from Author) + // But when accessed directly, Post should have author + type PostFlatModel = Schema['Post']['__meta']['flatModel']; + + // Post's direct flatModel should have author (it's not cyclical from Post's perspective) + type HasAuthorField = 'author' extends keyof PostFlatModel ? true : false; + type test1 = Expect>; + + // But when accessed through Author -> Post, the author field should be omitted + type AuthorFlatModel = Schema['Author']['__meta']['flatModel']; + type PostFromAuthor = + AuthorFlatModel['posts'] extends Array ? P : never; + type PostFromAuthorHasAuthor = 'author' extends keyof PostFromAuthor + ? true + : false; + type test2 = Expect>; + }); + }); + + describe('multiple relationships to different models are preserved', () => { + // Schema: User has posts and comments (different relationships) + const schema = a.schema({ + User: a.model({ + name: a.string().required(), + posts: a.hasMany('Post', 'userId'), + comments: a.hasMany('Comment', 'userId'), + }), + Post: a.model({ + title: a.string(), + userId: a.id(), + user: a.belongsTo('User', 'userId'), + }), + Comment: a.model({ + content: a.string(), + userId: a.id(), + user: a.belongsTo('User', 'userId'), + }), + }); + + type Schema = ClientSchema; + + test('FlatModel preserves all hasMany relationships to different models', () => { + type UserFlatModel = Schema['User']['__meta']['flatModel']; + + // User should have both posts and comments + type HasPostsField = 'posts' extends keyof UserFlatModel ? true : false; + type HasCommentsField = 'comments' extends keyof UserFlatModel + ? true + : false; + + type test1 = Expect>; + type test2 = Expect>; + }); + + test('each relationship path is independently traversable', () => { + type UserFlatModel = Schema['User']['__meta']['flatModel']; + + // Posts should have their own fields + type PostElement = + UserFlatModel['posts'] extends Array ? P : never; + type PostHasTitle = 'title' extends keyof PostElement ? true : false; + type test1 = Expect>; + + // Comments should have their own fields + type CommentElement = + UserFlatModel['comments'] extends Array ? C : never; + type CommentHasContent = 'content' extends keyof CommentElement + ? true + : false; + type test2 = Expect>; + }); + }); + + describe('traversal through child to different parent is preserved', () => { + // Schema: Author -> Post -> Category (Post belongs to both Author and Category) + const schema = a.schema({ + Author: a.model({ + name: a.string().required(), + posts: a.hasMany('Post', 'authorId'), + }), + Category: a.model({ + name: a.string().required(), + posts: a.hasMany('Post', 'categoryId'), + }), + Post: a.model({ + title: a.string(), + authorId: a.id(), + categoryId: a.id(), + author: a.belongsTo('Author', 'authorId'), + category: a.belongsTo('Category', 'categoryId'), + }), + }); + + type Schema = ClientSchema; + + test('traversal from Author -> Post -> Category is preserved', () => { + type AuthorFlatModel = Schema['Author']['__meta']['flatModel']; + type PostFromAuthor = + AuthorFlatModel['posts'] extends Array ? P : never; + + // Post accessed from Author should have category (non-cyclical path to different model) + type HasCategoryField = 'category' extends keyof PostFromAuthor + ? true + : false; + type test = Expect>; + }); + + test('traversal from Category -> Post -> Author is preserved', () => { + type CategoryFlatModel = Schema['Category']['__meta']['flatModel']; + type PostFromCategory = + CategoryFlatModel['posts'] extends Array ? P : never; + + // Post accessed from Category should have author (non-cyclical path to different model) + type HasAuthorField = 'author' extends keyof PostFromCategory + ? true + : false; + type test = Expect>; + }); + + test('cyclical paths back to same parent are still omitted', () => { + type AuthorFlatModel = Schema['Author']['__meta']['flatModel']; + type PostFromAuthor = + AuthorFlatModel['posts'] extends Array ? P : never; + + // Post accessed from Author should NOT have author (cyclical back to Author) + type HasAuthorField = 'author' extends keyof PostFromAuthor + ? true + : false; + type test = Expect>; + }); + }); + + describe('scalar and non-relationship fields are always preserved', () => { + const schema = a.schema({ + Post: a.model({ + title: a.string().required(), + content: a.string(), + viewCount: a.integer(), + isPublished: a.boolean(), + tags: a.string().array(), + comments: a.hasMany('Comment', 'postId'), + }), + Comment: a.model({ + content: a.string(), + postId: a.id(), + post: a.belongsTo('Post', 'postId'), + }), + }); + + type Schema = ClientSchema; + + test('all scalar fields are preserved in FlatModel', () => { + type PostFlatModel = Schema['Post']['__meta']['flatModel']; + + type HasTitle = 'title' extends keyof PostFlatModel ? true : false; + type HasContent = 'content' extends keyof PostFlatModel ? true : false; + type HasViewCount = 'viewCount' extends keyof PostFlatModel + ? true + : false; + type HasIsPublished = 'isPublished' extends keyof PostFlatModel + ? true + : false; + type HasTags = 'tags' extends keyof PostFlatModel ? true : false; + + type test1 = Expect>; + type test2 = Expect>; + type test3 = Expect>; + type test4 = Expect>; + type test5 = Expect>; + }); + + test('system fields are preserved in FlatModel', () => { + type PostFlatModel = Schema['Post']['__meta']['flatModel']; + + type HasId = 'id' extends keyof PostFlatModel ? true : false; + type HasCreatedAt = 'createdAt' extends keyof PostFlatModel + ? true + : false; + type HasUpdatedAt = 'updatedAt' extends keyof PostFlatModel + ? true + : false; + + type test1 = Expect>; + type test2 = Expect>; + type test3 = Expect>; + }); + + test('nested model scalar fields are preserved', () => { + type PostFlatModel = Schema['Post']['__meta']['flatModel']; + type CommentElement = + PostFlatModel['comments'] extends Array ? C : never; + + // Explicitly defined fields are preserved + type HasContent = 'content' extends keyof CommentElement ? true : false; + type HasPostId = 'postId' extends keyof CommentElement ? true : false; + + type test1 = Expect>; + type test2 = Expect>; + + // Note: Implicit identifier (id) and system fields (createdAt, updatedAt) + // are added at the FlatClientFields level, not during nested field resolution. + // Nested models resolved through relationships only include explicitly defined fields. + }); + }); +}); diff --git a/packages/data-schema/__tests__/FlatModel.test.ts b/packages/data-schema/__tests__/FlatModel.test.ts new file mode 100644 index 000000000..10cbc8b6e --- /dev/null +++ b/packages/data-schema/__tests__/FlatModel.test.ts @@ -0,0 +1,14 @@ +/** + * Runtime tests for FlatModel type infrastructure. + * Type-level tests are in FlatModel.test-d.ts + */ +import { expectTypeTestsToPassAsync } from 'jest-tsd'; +import path from 'path'; + +describe('FlatModel type tests', () => { + it('should pass all type-level tests', async () => { + await expectTypeTestsToPassAsync( + path.resolve(__dirname, 'FlatModel.test-d.ts'), + ); + }); +}); diff --git a/packages/data-schema/src/ClientSchema/Core/ClientModel.ts b/packages/data-schema/src/ClientSchema/Core/ClientModel.ts index 2fd79c641..973a3e275 100644 --- a/packages/data-schema/src/ClientSchema/Core/ClientModel.ts +++ b/packages/data-schema/src/ClientSchema/Core/ClientModel.ts @@ -6,7 +6,11 @@ import type { } from '../../ModelType'; import type { ClientSchemaProperty } from './ClientSchemaProperty'; import type { Authorization, ImpliedAuthFields } from '../../Authorization'; -import type { SchemaMetadata, ResolveFields } from '../utilities'; +import type { + SchemaMetadata, + ResolveFields, + FlatResolveFields, +} from '../utilities'; import type { IsEmptyStringOrNever, UnionToIntersection, @@ -50,6 +54,8 @@ export interface ClientModel< secondaryIndexes: IndexQueryMethodsFromIR; __meta: { listOptionsPkParams: ListOptionsPkParams; + rawType: T; + flatModel: FlatClientFields; disabledOperations: DisabledOpsToMap; }; } @@ -91,6 +97,31 @@ type ClientFields< AuthFields & Omit, keyof ResolveFields>; +/** + * Flattened client fields type used for selection set path generation. + * Uses FlatResolveFields to short-circuit bi-directional relationships, + * preventing cyclical path generation that causes TS2590 errors. + * + * @param Bag - The top-level ClientSchema for resolving references + * @param Metadata - Schema metadata containing auth fields + * @param IsRDS - Whether this is an RDS model + * @param T - The model type parameter shape + * @param K - The model name key for cycle detection + */ +type FlatClientFields< + Bag extends Record, + Metadata extends SchemaMetadata, + IsRDS extends boolean, + T extends ModelTypeParamShape, + K extends keyof Bag & string, +> = FlatResolveFields & + If, ImplicitIdentifier> & + AuthFields & + Omit< + SystemFields, + keyof FlatResolveFields + >; + type SystemFields = IsRDS extends false ? { readonly createdAt: string; diff --git a/packages/data-schema/src/ClientSchema/utilities/ResolveField.ts b/packages/data-schema/src/ClientSchema/utilities/ResolveField.ts index 8da96ff0a..fb293c6ae 100644 --- a/packages/data-schema/src/ClientSchema/utilities/ResolveField.ts +++ b/packages/data-schema/src/ClientSchema/utilities/ResolveField.ts @@ -8,6 +8,7 @@ import { CustomType } from '../../CustomType'; import { RefType, RefTypeParamShape } from '../../RefType'; import { ResolveRef } from './ResolveRef'; import { LazyLoader } from '../../runtime'; +import type { ExtendsNever } from '../../util'; /** * Takes a `ReturnType` and turns it into a client-consumable type. Fields @@ -38,13 +39,79 @@ type ShallowPretty = { [K in keyof T]: T[K]; }; -export type ResolveIndividualField, T> = +/** + * Resolves model fields with parent model name tracking for cycle detection. + * Used to generate FlatModel types that short-circuit bi-directional relationships. + * + * @param Bag - The top-level ClientSchema for resolving references + * @param T - The fields to resolve + * @param FlatModelName - The name of the parent model for cycle detection + * @param Raw - The raw field metadata for detecting relationship types + */ +export type FlatResolveFields< + Bag extends Record, + T, + FlatModelName extends keyof Bag & string = never, + Raw extends Record = Record, +> = ShallowPretty< + ShortCircuitBiDirectionalRelationship< + { + [K in keyof T as IsRequired extends true + ? K + : never]: ResolveIndividualField; + } & { + [K in keyof T as IsRequired extends true + ? never + : K]+?: ResolveIndividualField; + }, + FlatModelName, + Raw + > +>; + +/** + * Filters out `belongsTo` relationship fields that reference the parent model. + * This prevents cyclical path generation in selection sets. + * + * @param Model - The resolved model fields + * @param ParentModelName - The name of the parent model to detect cycles + * @param Raw - The raw field metadata containing relationship information + */ +export type ShortCircuitBiDirectionalRelationship< + Model extends Record, + ParentModelName extends string, + Raw extends Record, +> = + ExtendsNever extends true + ? Model + : { + [Field in keyof Model as Field extends keyof Raw + ? Raw[Field] extends ModelRelationshipField< + infer RelationshipShape extends ModelRelationshipFieldParamShape, + any, + any, + any + > + ? RelationshipShape['relationshipType'] extends 'belongsTo' + ? RelationshipShape['relatedModel'] extends ParentModelName + ? never // Short-circuit: omit this field + : Field + : Field + : Field + : Field]: Model[Field]; + }; + +export type ResolveIndividualField< + Bag extends Record, + T, + FlatModelName extends keyof Bag & string = never, +> = T extends BaseModelField ? FieldShape : T extends RefType ? ResolveRef : T extends ModelRelationshipField - ? ResolveRelationship + ? ResolveRelationship : T extends CustomType ? ResolveFields | null : T extends EnumType @@ -52,20 +119,73 @@ export type ResolveIndividualField, T> = : never; /** - * Resolves to never if the related model has disabled list or get ops for hasOne/hasMany or belongsTo respectively + * Resolves relationship fields to either LazyLoader (standard resolution) or + * inline types with cycle detection (flat model resolution). + * + * When ParentModelName is provided (flat model resolution): + * - Uses ShortCircuitBiDirectionalRelationship to omit cyclical belongsTo fields + * - Resolves relationships inline instead of using LazyLoader + * + * When ParentModelName is never (standard resolution): + * - Uses LazyLoader for relationship fields + * - Resolves to never if the related model has disabled list/get ops */ type ResolveRelationship< Bag extends Record, RelationshipShape extends ModelRelationshipFieldParamShape, + ParentModelName extends keyof Bag & string = never, > = - DependentLazyLoaderOpIsAvailable extends true - ? LazyLoader< - RelationshipShape['valueRequired'] extends true - ? Bag[RelationshipShape['relatedModel']]['type'] - : Bag[RelationshipShape['relatedModel']]['type'] | null, - RelationshipShape['array'] - > - : never; + ExtendsNever extends true + ? // Standard resolution with LazyLoader + DependentLazyLoaderOpIsAvailable extends true + ? LazyLoader< + RelationshipShape['valueRequired'] extends true + ? Bag[RelationshipShape['relatedModel']]['type'] + : Bag[RelationshipShape['relatedModel']]['type'] | null, + RelationshipShape['array'] + > + : never + : // Flat model resolution with cycle detection + RelationshipShape['array'] extends true + ? Array< + FlatResolveRelatedModel< + Bag, + RelationshipShape['relatedModel'], + ParentModelName + > + > + : RelationshipShape['valueRequired'] extends true + ? FlatResolveRelatedModel< + Bag, + RelationshipShape['relatedModel'], + ParentModelName + > + : FlatResolveRelatedModel< + Bag, + RelationshipShape['relatedModel'], + ParentModelName + > | null; + +/** + * Resolves a related model for flat model generation. + * Re-resolves the related model's fields with the parent model name for proper + * cycle detection. This ensures that when traversing Author -> Post, the Post's + * `author` belongsTo field is correctly short-circuited. + * + * Note: We cannot use the pre-computed flatModel from __meta because it was + * computed with the related model as the parent, not the current traversal parent. + */ +type FlatResolveRelatedModel< + Bag extends Record, + RelatedModelName extends string, + ParentModelName extends string, +> = RelatedModelName extends keyof Bag + ? Bag[RelatedModelName] extends { __meta: { rawType: infer RT } } + ? RT extends { fields: infer F } + ? FlatResolveFields> + : Bag[RelatedModelName]['type'] + : Bag[RelatedModelName]['type'] + : never; type DependentLazyLoaderOpIsAvailable< Bag extends Record, diff --git a/packages/data-schema/src/util/index.ts b/packages/data-schema/src/util/index.ts index a48a3b8ce..4e71d0675 100644 --- a/packages/data-schema/src/util/index.ts +++ b/packages/data-schema/src/util/index.ts @@ -7,3 +7,12 @@ export { Select } from './Select'; export type * from './Filters'; export type * from './IndexShapes'; export type * from './Rename'; + +/** + * Utility type that checks if a type extends `never`. + * Returns `true` if T is `never`, `false` otherwise. + * + * This is useful for conditional type logic where we need to + * detect if a generic parameter was not provided (defaults to never). + */ +export type ExtendsNever = [T] extends [never] ? true : false;