Skip to content
This repository was archived by the owner on May 19, 2025. It is now read-only.

Commit 68bbf74

Browse files
committed
Need to dry things up but top-level inclues work
1 parent 6cbe2ec commit 68bbf74

12 files changed

Lines changed: 148 additions & 38 deletions

File tree

addon/adapters/firestore.ts

Lines changed: 85 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { pluralize } from 'ember-inflector';
44
import { get, set } from '@ember/object';
55
import { inject as service } from '@ember/service';
66
import { camelize } from '@ember/string';
7-
import RSVP from 'rsvp';
7+
import RSVP, { resolve } from 'rsvp';
88
import Ember from 'ember';
99
import FirebaseAppService from '../services/firebase-app';
1010
import ModelRegistry from 'ember-data/types/registries/model';
@@ -117,12 +117,14 @@ export default class FirestoreAdapter extends DS.Adapter.extend({
117117
// @ts-ignore repeat here for the tyepdocs
118118
firebaseApp: Ember.ComputedProperty<FirebaseAppService, FirebaseAppService>;
119119

120-
findRecord<K extends keyof ModelRegistry>(_store: DS.Store, type: ModelRegistry[K], id: string) {
121-
return rootCollection(this, type).then(ref => ref.doc(id).get());
120+
findRecord<K extends keyof ModelRegistry>(store: DS.Store, type: ModelRegistry[K], id: string, snapshot: any) {
121+
return rootCollection(this, type).then(ref =>
122+
includeRelationships(ref.doc(id).get(), store, this, snapshot, type)
123+
);
122124
}
123125

124126
findAll<K extends keyof ModelRegistry>(store: DS.Store, type: ModelRegistry[K]) {
125-
return this.query(store, type)
127+
return this.query(store, type);
126128
}
127129

128130
findHasMany<K extends keyof ModelRegistry>(store: DS.Store, snapshot: DS.Snapshot<K>, url: string, relationship: {[key:string]: any}) {
@@ -145,22 +147,25 @@ export default class FirestoreAdapter extends DS.Adapter.extend({
145147
}
146148
}
147149

148-
query<K extends keyof ModelRegistry>(_store: DS.Store, type: ModelRegistry[K], options?: QueryOptions, _recordArray?: DS.AdapterPopulatedRecordArray<any>) {
149-
return rootCollection(this, type).then(collection => queryDocs(collection, queryOptionsToQueryFn(options)));
150+
query<K extends keyof ModelRegistry>(store: DS.Store, type: ModelRegistry[K], options?: QueryOptions, _recordArray?: DS.AdapterPopulatedRecordArray<any>) {
151+
return rootCollection(this, type).then(collection =>
152+
queryDocs(collection, queryOptionsToQueryFn(options))
153+
).then(q => includeCollectionRelationships(q, store, this, options, type));
150154
}
151155

152-
queryRecord<K extends keyof ModelRegistry>(_store: DS.Store, type: ModelRegistry[K], options?: QueryOptions|QueryRecordOptions) {
156+
queryRecord<K extends keyof ModelRegistry>(store: DS.Store, type: ModelRegistry[K], options?: QueryOptions|QueryRecordOptions) {
153157
return rootCollection(this, type).then((ref:firestore.CollectionReference) => {
154158
const queryOrRef = queryRecordOptionsToQueryFn(options)(ref);
155159
if (isQuery(queryOrRef)) {
156160
if (queryOrRef.limit) { throw "Dont specify limit on queryRecord" }
157161
return queryOrRef.limit(1).get();
158162
} else {
159-
return queryOrRef.get() as any; // TODO fix the types here, they're a little broken
163+
(options as any).id = queryOrRef.id;
164+
return includeRelationships(queryOrRef.get() as any, store, this, options, type); // TODO fix the types here, they're a little broken
160165
}
161166
}).then((snapshot:firestore.QuerySnapshot|firestore.DocumentSnapshot) => {
162167
if (isQuerySnapshot(snapshot)) {
163-
return snapshot.docs[0];
168+
return includeRelationships(resolve(snapshot.docs[0]), store, this, options, type);
164169
} else {
165170
return snapshot;
166171
}
@@ -195,7 +200,6 @@ export default class FirestoreAdapter extends DS.Adapter.extend({
195200

196201
}
197202

198-
199203
export type CollectionReferenceOrQuery = firestore.CollectionReference | firestore.Query;
200204
export type QueryFn = (ref: CollectionReferenceOrQuery) => CollectionReferenceOrQuery;
201205
export type QueryRecordFn = (ref: firestore.CollectionReference) => firestore.DocumentReference;
@@ -302,6 +306,77 @@ const getFirestore = (adapter: FirestoreAdapter) => {
302306
return cachedFirestoreInstance!;
303307
};
304308

309+
const includeCollectionRelationships = (collection: firestore.QuerySnapshot, store: DS.Store, adapter: FirestoreAdapter, snapshot: any, type: any): Promise<firestore.QuerySnapshot> => {
310+
if (snapshot && snapshot.include) {
311+
const includes = snapshot.include.split(',') as Array<string>;
312+
const relationshipsToInclude = includes.map(e => type.relationshipsByName.get(e) as {[key:string]: any}).filter(r => !!r && !r.options.embedded);
313+
return Promise.all(
314+
relationshipsToInclude.map(r => {
315+
if (r.meta.kind == 'hasMany') {
316+
return Promise.all(collection.docs.map(d => adapter.findHasMany(store, { id: d.id } as any, '', r)))
317+
} else {
318+
const belongsToIds = [...new Set(collection.docs.map(d => d.data()[r.meta.key]).filter(id => !!id))]
319+
return Promise.all(belongsToIds.map(id => adapter.findBelongsTo(store, { id } as any, '', r)))
320+
}
321+
})
322+
).then(allIncludes => {
323+
relationshipsToInclude.forEach((r: any, i:number) => {
324+
const relationship = r.meta;
325+
const pluralKey = pluralize(relationship.key);
326+
const key = relationship.kind == 'belongsTo' ? relationship.key : pluralKey;
327+
const includes = allIncludes[i];
328+
collection.docs.forEach(doc => {
329+
if (relationship.kind == 'belongsTo') {
330+
const result = includes.find((r:any) => r.id == doc.data()[key]);
331+
if (result) {
332+
if (!(doc as any)._document._included) { (doc as any)._document._included = {} }
333+
(doc as any)._document._included[key] = result;
334+
}
335+
} else {
336+
if (!(doc as any)._document._included) { (doc as any)._document._included = {} }
337+
(doc as any)._document._included[pluralKey] = includes;
338+
}
339+
});
340+
});
341+
return collection;
342+
});
343+
} else {
344+
return resolve(collection);
345+
}
346+
}
347+
348+
const includeRelationships = <T=any>(promise: Promise<T>, store: DS.Store, adapter: FirestoreAdapter, snapshot: any, type: any): Promise<T> => {
349+
if (snapshot && snapshot.include) {
350+
const includes = snapshot.include.split(',') as Array<string>;
351+
const relationshipsToInclude = includes.map(e => type.relationshipsByName.get(e) as {[key:string]: any}).filter(r => !!r && !r.options.embedded);
352+
const hasManyRelationships = relationshipsToInclude.filter(r => r.meta.kind == 'hasMany');
353+
const belongsToRelationships = relationshipsToInclude.filter(r => r.meta.kind == 'belongsTo');
354+
return Promise.all([
355+
promise,
356+
...hasManyRelationships.map(r => adapter.findHasMany(store, snapshot, '', r))
357+
]).then(([doc, ...includes]) => {
358+
doc._document._included = hasManyRelationships.reduce((c, e, i) => {
359+
c[pluralize(e.key)] = includes[i];
360+
return c;
361+
}, {});
362+
return Promise.all([
363+
resolve(doc),
364+
...belongsToRelationships.filter(r => !!doc.data()[r.meta.key]).map(r => {
365+
return adapter.findBelongsTo(store, { id: doc.data()[r.meta.key] } as any, '', r)
366+
})
367+
]);
368+
}).then(([doc, ...includes]) => {
369+
doc._document._included = { ...doc._document._included, ...belongsToRelationships.reduce((c, e, i) => {
370+
c[e.key] = includes[i];
371+
return c;
372+
}, {})};
373+
return doc;
374+
});
375+
} else {
376+
return promise;
377+
}
378+
}
379+
305380
declare module 'ember-data' {
306381
interface AdapterRegistry {
307382
'firestore': FirestoreAdapter;

addon/serializers/firestore.ts

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import DS from 'ember-data';
22
import { firestore } from 'firebase/app';
3+
// @ts-ignore
4+
import { singularize } from 'ember-inflector';
35

46
export type DocumentSnapshot = firestore.DocumentSnapshot | firestore.QueryDocumentSnapshot;
57
export type Snapshot = firestore.DocumentSnapshot | firestore.QuerySnapshot;
@@ -38,16 +40,6 @@ declare module 'ember-data' {
3840
}
3941
}
4042

41-
export const normalize = (store: DS.Store, modelClass: DS.Model, snapshot: DocumentSnapshot) => {
42-
const id = snapshot.id;
43-
const type = (<any>modelClass).modelName;
44-
const _ref = snapshot.ref;
45-
const attributes = { ...snapshot.data()!, _ref };
46-
const { relationships, included } = normalizeRelationships(store, modelClass, attributes);
47-
const data = { id, type, attributes, relationships };
48-
return { data, included };
49-
}
50-
5143
function isQuerySnapshot(arg: any): arg is firestore.QuerySnapshot {
5244
return arg.query !== undefined;
5345
}
@@ -65,15 +57,28 @@ const normalizeRelationships = (store: DS.Store, modelClass: DS.Model, attribute
6557
const relationships: {[field:string]: any} = {};
6658
const included: any[] = [];
6759
modelClass.eachRelationship((key: string, relationship: any) => {
68-
const attribute = attributes[key];
69-
delete attributes[key];
60+
const attribute = attributes.data()[key];
61+
const payload = attributes._document && attributes._document._included && attributes._document._included[key];
62+
if (payload) {
63+
const modelName = singularize(relationship.key) as never;
64+
const modelClass = store.modelFor(modelName);
65+
const serializer = store.serializerFor(modelName) as any;
66+
const { data } = relationship.kind === 'belongsTo' ? serializer.normalizeSingleResponse(store, modelClass, payload) : serializer.normalizeArrayResponse(store, modelClass, payload);
67+
if (Array.isArray(data)) {
68+
data.forEach((r:any) => {
69+
return included.splice(-1, 0, { links: { self: 'emberfire' }, ...r })
70+
});
71+
} else {
72+
included.splice(-1, 0, { links: { self: 'emberfire' }, ...data });
73+
}
74+
}
7075
relationships[key] = normalizeRealtionship(relationship)(store, attribute, relationship, included);
7176
}, null);
7277
return {relationships, included};
7378
}
7479

7580
const normalizeRealtionship = (relationship: any) => {
76-
if (relationship.kind === 'belongsTo') {
81+
if (relationship.kind == 'belongsTo') {
7782
return normalizeBelongsTo;
7883
} else if (relationship.options.subcollection) {
7984
return normalizeHasMany; // this is handled in the adapter
@@ -105,11 +110,32 @@ const normalizeEmbedded = (store: DS.Store, attribute: any, relationship: any, i
105110
const data = included
106111
.filter(record => record.type == relationship.type)
107112
.map(record => ({ id: record.id, type: record.type }));
108-
return { links: { related: 'emberfire' }, data };
113+
if (data.length > 0 ) {
114+
return { links: { related: 'emberfire' }, data };
115+
} else {
116+
return { links: { related: 'emberfire' } };
117+
}
109118
} else {
110119
return { };
111120
}
112121
}
113122

114-
const normalizeHasMany = (_store: DS.Store, _attribute: any, _relationship: any, _included: any[]) =>
115-
({ links: { related: 'emberfire' } })
123+
const normalizeHasMany = (_store: DS.Store, _payload: firestore.QuerySnapshot, relationship: any, included: any[]) => {
124+
const relevantIncluded = included.filter(i => i.type == singularize(relationship.key));
125+
const data = relevantIncluded.map((r:any) => ({ type: r.type, id: r.id }));
126+
if (data.length > 0) {
127+
return { links: { related: 'emberfire' }, data };
128+
} else {
129+
return { links: { related: 'emberfire' } };
130+
}
131+
}
132+
133+
export const normalize = (store: DS.Store, modelClass: DS.Model, snapshot: DocumentSnapshot) => {
134+
const id = snapshot.id;
135+
const type = (<any>modelClass).modelName;
136+
const _ref = snapshot.ref;
137+
const attributes = { ...snapshot.data()!, _ref };
138+
const { relationships, included } = normalizeRelationships(store, modelClass, snapshot);
139+
const data = { id, type, attributes, relationships };
140+
return { data, included };
141+
}

addon/services/realtime-listener.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,14 +92,17 @@ export default class RealtimeListenerService extends Service.extend({
9292
const args = { model, store, modelName, modelClass, uniqueIdentifier, serializer, adapter };
9393
if (query) {
9494
if (isFirestoreQuery(query)) {
95+
// Firestore query
9596
const unsubscribe = runFirestoreCollectionListener({query, ...args});
9697
setRouteSubscription(this, route, uniqueIdentifier, unsubscribe);
9798
} else {
99+
// RTDB query
98100
const unsubscribe = runRealtimeDatabaseListListener({ref: query, ...args});
99101
setRouteSubscription(this, route, uniqueIdentifier, unsubscribe);
100102
}
101103
} else if (ref) {
102104
if (isFirestoreDocumentRefernce(ref)) {
105+
// Firestore find
103106
const unsubscribe = ref.onSnapshot(doc => {
104107
run(() => {
105108
const normalizedData = serializer.normalizeSingleResponse(store, modelClass, doc);
@@ -108,6 +111,7 @@ export default class RealtimeListenerService extends Service.extend({
108111
});
109112
setRouteSubscription(this, route, uniqueIdentifier, unsubscribe);
110113
} else {
114+
// RTDB find
111115
const listener = ref.on('value', snapshot => {
112116
run(() => {
113117
if (snapshot) {
@@ -125,13 +129,15 @@ export default class RealtimeListenerService extends Service.extend({
125129
setRouteSubscription(this, route, uniqueIdentifier, unsubscribe);
126130
}
127131
} else {
128-
// this might be a findAll, findAll strips metadata :(
132+
// findAll ditches the metadata :(
129133
if (serializer.constructor.name == 'FirestoreSerializer') {
134+
// Firestore findAll
130135
firestoreRootCollection(adapter, modelName).then(query => {
131136
const unsubscribe = runFirestoreCollectionListener({query, ...args});
132137
setRouteSubscription(this, route, uniqueIdentifier, unsubscribe);
133138
});
134139
} else if (serializer.constructor.name == 'RealtimeDatabaseSerializer') {
140+
// RTDB findAll
135141
realtimeDatabaseRootCollection(adapter, modelName).then(ref => {
136142
const unsubscribe = runRealtimeDatabaseListListener({ref, ...args});
137143
setRouteSubscription(this, route, uniqueIdentifier, unsubscribe);

tests/dummy/app/models/tag.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import DS from 'ember-data';
22

3-
const { attr } = DS;
3+
const { attr, belongsTo } = DS;
44

55
export default DS.Model.extend({
6-
name: attr('string')
6+
name: attr('string'),
7+
comment: belongsTo('comment')
78
});

tests/dummy/app/models/thought.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import DS from 'ember-data';
22

3-
const { attr, hasMany } = DS;
3+
const { attr, hasMany, belongsTo } = DS;
44

55
export default DS.Model.extend({
66
musing: attr('string'),
7+
user: belongsTo('user'),
78
tags: hasMany('tag', { embedded: true })
89
});

tests/dummy/app/routes/comment.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@ import RealtimeRouteMixin from 'emberfire/mixins/realtime-route';
33

44
export default Route.extend(RealtimeRouteMixin, {
55
model(params) {
6-
return this.store.findRecord('comment', params.id);
6+
return this.store.findRecord('comment', params.id, { include: 'user' });
77
}
88
})

tests/dummy/app/routes/comments.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
import Route from '@ember/routing/route';
22
import RealtimeRouteMixin from 'emberfire/mixins/realtime-route';
33
import { inject as service } from '@ember/service';
4-
import RSVP, { reject } from 'rsvp';
54

65
export default Route.extend(RealtimeRouteMixin, {
76
firebaseApp: service(),
87
model() {
98
return this.firebaseApp.auth().then(({currentUser}) =>
109
currentUser &&
11-
this.store.query('comment', { filter: { user: currentUser.uid } }) ||
12-
this.store.findAll('comment')
10+
this.store.query('comment', { filter: { user: currentUser.uid }, include: 'user' }) ||
11+
this.store.query('comment', { include: 'user'} )
1312
);
1413
}
15-
})
14+
})

tests/dummy/app/routes/something.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export default Route.extend(RealtimeRouteMixin, {
66
model(params) {
77
return this.store.queryRecord('something', {
88
doc: ref => ref.doc(params.id),
9-
include: ['comments,comments.user']
9+
include: 'user,comments'
1010
});
1111
},
1212
afterModel(model) {

tests/dummy/app/routes/somethings.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@ import RealtimeRouteMixin from 'emberfire/mixins/realtime-route';
33

44
export default Route.extend(RealtimeRouteMixin, {
55
model() {
6-
return this.store.query('something', { orderBy: 'title' });
6+
return this.store.query('something', { orderBy: 'title', include: 'user' });
77
}
88
})

tests/dummy/app/routes/user.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@ import RealtimeRouteMixin from 'emberfire/mixins/realtime-route';
33

44
export default Route.extend(RealtimeRouteMixin, {
55
model(params) {
6-
return this.store.findRecord('user', params.id);
6+
return this.store.findRecord('user', params.id, {include: 'thoughts,comments,somethings'});
77
}
88
});

0 commit comments

Comments
 (0)