How to get a full list of loopback4 models to evaluate custom decorator metadata at boot time? - loopbackjs

I'm currently migrating a loopback3 application to loopback4. I've annotated the properties in my loopback4 models with a custom typescript decorator with some metadata.
How can I get a full list of models and evaluate their metadata at boot time?
I did some experiments with LifeCycleObserver, but did not find a way how to get notified when all models are ready nor get a list of them.
Here is an simplified example of the metadata i want to process. Please note: #propertyAcl is a custom decorator.
export class Model1 extends AuditMixin(SoftdeleteMixin(AbstractEntity)) {
// ...
#property()
#propertyAcl({
'role1': Permission.READ_WRITE,
'role2': Permission.READONLY,
})
myproperty?: string;
// ...
}
I need to configure the external library accesscontrol with the metadata at boot time. The idea is to create property based acls per model.

How can I get a full list of models and evaluate their metadata at boot time?
Overview
Using #loopback/metadata to create a Decorator and define / get the metadata via MetadataInspector.
Create a DecoratorFactory "A"
Create a Context "B"
Overide the mergeWithInherited function in "A", and in this function we store (define) the metadata of the property that you annotated at design time to "B"
Create a Decorator "C" via "A"
Annotate any properties you want with "C"
Get the metadata in "B" at runtime
Step 1 2 3 4
// 1
class A extends PropertyDecoratorFactory<object> {
constructor(key: string, spec: object, options?: DecoratorOptions | undefined) {
super(key, spec, options);
}
// 3
mergeWithInherited(inheritedMetadata: MetadataMap<object>, target: Object, propertyName?: string, descriptorOrIndex?: TypedPropertyDescriptor<any> | number): any {
// define metadata to "B"
MetadataInspector.DesignTimeReflector.defineMetadata(`${target.constructor.name}.${propertyName}`, this.spec, B);
}
}
// 2
export const B = new Context("B.context");
// 4
export function C(spec: object): PropertyDecorator {
return A.createDecorator<object>(
'C.decorator',
spec,
);
}
Step 5
class ModelFoo extends Model {
#property({
type: 'string'
})
p1?: string;
#C({ metadata: "this is ModelFoo" })
#property({
type: 'string'
})
p2?: string;
}
class ModelBar extends Model {
#property({
type: 'string'
})
p1?: string;
#C({ metadata: "this is ModelBar" })
#property({
type: 'string'
})
p2?: string;
}
Step 6
function Test() {
// get all metadata keys in "B"
let keys: string[] = MetadataInspector.DesignTimeReflector.getMetadataKeys(B);
// output >>>> [ 'ModelFoo.p2', 'ModelBar.p2' ]
console.log(keys);
// get metadata with 'ModelFoo.p2' in "B"
let metadata = MetadataInspector.DesignTimeReflector.getMetadata(keys[0], B);
// output >>>> { metadata: 'this is ModelFoo' }
console.log(metadata);
// do somthing...
}

Related

AWS Amplify searchable field by geo distance (location)

I'm developing an application that will allow users to search for other users by ordering them by geo location.
According to the information found on the internet, to do this on amplify I have to perform the following steps:
create an elastic search mapping, indicating the type 'geo_point' on the lastPosition attribute (I would like to define it once in the project file and not at each build in the elastic search console)
create a custom query and a custom vtl resolver and set the sort by lastPosition (of type '_geo_distance') in it.
But I don't understand how to do these 2 steps, so I need some suggestion :(.
// my schema.graphql
type User
#model
#auth(rules: [{allow: owner}, {allow: private, operations: [read]}])
#searchable {
// ...other fields
id: ID!
lastPosition: Position
positionUpdatedAt: AWSDateTime
}
type Position {
lat: Float!
lon: Float!
}
// my custom searchUsers query in graphql/custom_queries.ts
export const searchUsersNearby = /* GraphQL */ `
query SearchUsersNearby(
$filter: SearchableUserFilterInput
$sort: [SearchableUserSortInput]
$location: PositionInput // current user position that i need to use in vtl resolver to sort users by distance,
$limit: Int
$nextToken: String
$from: Int
$aggregates: [SearchableUserAggregationInput]
) {
searchUsersNearby(
filter: $filter
sort: $sort
limit: $limit
nextToken: $nextToken
from: $from
aggregates: $aggregates
) {
items {
id
// ...other fields
lastPosition
positionUpdatedAt
createdAt
updatedAt
owner
}
nextToken
total
aggregateItems {
name
result {
... on SearchableAggregateScalarResult {
value
}
... on SearchableAggregateBucketResult {
buckets {
key
doc_count
}
}
}
}
}
}
`;
I use amplify v8.1.0 with transformer v2.
Thank you guys

Flutter - type 'List<dynamic>' is not a subtype of type 'List<Model>?'

I'm learning flutter by making an app following some youtube tutorials. I'm trying to make a listview of search results. I'm able to query and get data from node backend but there's this error while mapping the json to model.
The data I'm getting from api is like this:
{id: <uuid>,
userEmail: <email_string>,
profile: [{profileName: <profile_name_string>,
profileImage: <image_url_string>,
profileBio: <profile_bio_string>}]
}
With the new model class I made following an answer here I'm able to get profile model separately but when i try to get account model with all profiles I'm getting the error:type 'List<dynamic>' is not a subtype of type 'List<ProfileModel>?'. The model class is:
class AccountModel {
String userId;
String userEmail;
String? userPassword;
final List<ProfileModel>? profile;
AccountModel({
required this.userId,
required this.userEmail,
this.userPassword,
this.profile,
});
factory AccountModel.fromJson({required Map<String, dynamic> map}) {
return AccountModel(
userId: map['id'],
userEmail: map['userEmail'],
userPassword: map['userPassword'],
profile: map['profile']
.map((profileJson) => ProfileModel.fromJson(profileJson))
.toList(),
);
}
}
class ProfileModel {
String profileName;
String profileImage;
String? profileBio;
ProfileModel({
required this.profileName,
required this.profileImage,
this.profileBio,
});
factory ProfileModel.fromJson(profileJson, {Map<String, dynamic>? map}) {
if (map != null) {
return ProfileModel(
profileName: map['profileName'],
profileImage: map['profileImage'] ?? "default",
profileBio: map['profileBio'],
);
} else {
return ProfileModel(
profileName: profileJson['profileName'],
profileImage: profileJson['profileImage'] ?? "default",
profileBio: profileJson['profileBio'],
);
}
}
}
How to make the list work?
You can use List.from() in this case.
profile: map['profile'] != null
? List<ProfileModel>.from(
map['profile']?.map((p) => ProfileModel.fromJson(p)))
: null)
We are using fromMap here on ProfileModel, you can simplify just separation while both are same on ProfileModel.
More about List and List.from.
When you declared the list here as
final List<ProfileModel>? profile;
It expects the list to have only ProfileModels as ListItem even though with "?". The way to solve it is either declared a list without generic ProfileModel :
final List? profile;
Or to typecast the item you're pushing as ProfileModel.
2. profile: map['profile'] .map((profileJson) => ProfileModel.fromJson(profileJson) as ProfileModel) .toList(),
I don't know the output structures and such so try to experiment with typecasting if the above code doesn't work. May be typecasting after toList() method as List can work too.

Passing variables to an external resolve reference

I'm using Apollo Federation for 2 months but I'm actually stuck. I've no idea how to pass a variable between my two graphql services.
I've got a website (website graphql service) which have orders (orders graphql service).
I have a query to find websites and for these websites I want some stats of orders for a date range. Here the typedef (website) :
type Query {
websites(orderFilter: OrderFilterInput): [Website!]
}
type Website #key(fields: "id") {
id: ID!
name: String!
url: String!
orderSummary(orderFilter: OrderSummaryFilterInput): OrderSummary
}
input OrderSummaryFilterInput {
beginDate: Date
endDate: Date
}
extend type OrderSummary #key(fields: "websiteId") {
websiteId: String! #external
}
The resolver :
orderSummary: (website, { orderSummaryFilter }) => {
console.log("orderSummaryFilter", orderSummaryFilter); // filters are OK
// HOW CAN I PASS orderFilterSummary to my order graphql service here ????
return { __typename: "OrderSummary", websiteId: website.id };
}
And Order graphql service
Typedef part :
type OrderSummary #key(fields: "websiteId") {
websiteId: String!
count: Int
amount: Int
}
Resolver part :
// order gql service
OrderSummary: {
__resolveReference(website, args, info) {
console.log("website id :", website.id); // I ve got my website ID
// HOW TO GET OrderSummaryFilter here ????
},
},
How can I access to order summary filter variable in order graphql resolver ? Thank you.
From what I am aware of, it is not possible to send variables from one service to another other then the ID. But there is a solution to this.
If you want to pass in variables, extend your Website type in your order service instead of extending order type in website service.
Order typedef:
extend type Website #key(fields: "id") {
id: ID! #external
orderSummary(orderFilter: OrderSummaryFilterInput): OrderSummary #requires(fields:"id")
}
Order resolver:
Website: {
orderSummary: async (website, { orderFilter }) => getOrderSummary(orderFilter) //get orderSummary with orderFilter
},
So I want to expound on the previous (and I believe correct) answer:
In Federation, you almost never should have to expose a field called somethingId (userId, websiteId, etc). That is often either a left-over from Schema Stitching, or you simply got your type origins backward. Instead of using somethingId, you should be able to just use the object. Often, moving the #extend to the other service will get rid of the somethingId field, and get rid of the type of problem you're currently facing:
Website Service:
type Query {
websites(orderFilter: OrderFilterInput): [Website!]
}
type Website #key(fields: "id") {
id: ID!
name: String!
url: String!
}
Order Service:
extend type Website #key(fields: "id") {
id: ID! #external
orderSummary(orderFilter: OrderSummaryFilterInput): OrderSummary
}
input OrderSummaryFilterInput {
beginDate: Date
endDate: Date
}
type OrderSummary {
website: Website!
count: Int
amount: Int
}
Resolvers:
const resolvers = {
Website: {
orderSummary(parent, args, context) {
const websiteId = parent.id;
// args is the data you wanted
}
},
};

ngxs: How to test dynamic selectors

I followed the official documentation on testing ngxs selectors (https://ngxs.gitbook.io/ngxs/recipes/unit-testing#testing-selectors), however it doesn't cover how to unittest dynamic selectors created with createSelector.
My normal selector just gets the state as an argument so I can easly test it by passing a prepared state and comparing the output.
#Selector()
static nachweise(state: NachweisStateModel) {
return state.nachweise;
}
//Setup state
const state = {...};
//Compare expectations
expect(NachweisState.nachweise(state)).toEqual(...);
My dynamic selector looks like this:
#Selector()
static nachweisById(id: string) {
return createSelector([NachweisState], state => {
return state.nachweise.find(nachweis => nachweis.id === id);
});
}
The only parameter it gets is the id by which it selects, but not the state. The State is automagically passed in by specifying it as the first parameter to createSelector and I don't know how I should test this selector.
It seems that the documentation has been updated:
it('should select requested animal names from state', () => {
const zooState = {
animals: [
{ type: 'zebra', name: 'Andy'},
{ type: 'panda', name: 'Betty'},
{ type: 'zebra', name: 'Crystal'},
{ type: 'panda', name: 'Donny'},
]
};
const value = ZooSelectors.animalNames('zebra')(zooState);
expect(value).toEqual(['Andy', 'Crystal']);
});

How do I add union type in Apollo graphql

I created this question in case anyone was curious on how to add union / Polymorphic types in Apollo. Hopefully this will make it easier for them.
In this example I wanted the response to either be a Worksheet or ApiError
// typedefs.js
export default [`
schema {
query: Query
}
type Query {
worksheet(id: String!): Worksheet | Error
}
type Worksheet {
id: String!
name String
}
type ApiError {
code: String!
message: String!
}
`];
// resolvers.js
export default {
Query: {
worksheet(_, args, { loaders }) {
return loaders.worksheet.get(args.id).catch(() => {
// ApiError
return {
code: '1',
message: 'test'
}
});
}
}
};
// Express Server
import { graphqlExpress } from 'apollo-server-express';
import { makeExecutableSchema } from 'graphql-tools';
import typeDefs from './typedefs';
import resolvers from './resolvers';
...
app.post(
'/graphql',
graphqlExpress(req => ({
makeExecutableSchema({ typeDefs, resolvers }),
context: mkRequestContext(req.ctx, req.log),
formatError: formatGraphQLError(req.ctx, req.log)
}))
);
In GraphQL to add a union type in the typedefs you have to define the union
i.e union WorksheetOrError = Worksheet | ApiError
// typedefs.js
export default [
`
schema {
query: Query
}
type Query {
worksheet(id: String!): WorksheetOrError
}
union WorksheetOrError = Worksheet | ApiError
type Worksheet {
id: String!
name String
}
type ApiError {
code: String!
message: String!
}
`];
In the resolvers you have to define a resolver for the union type that has the property __resolveType. This will help tell the GraphQL executor which type the result is.
// resolvers.js
export default {
Query: {
worksheet() {
...
}
},
WorksheetOrError: {
__resolveType(obj) {
if (obj.id) {
return 'Worksheet';
}
if (obj.code) {
return 'ApiError';
}
return null;
}
},
};
To create a GraphQL Query in Apollo Client
// Your application code.
// This is my Worksheet Query in my React code.
const WorksheetQuery = gql`
query GetWorksheet($worksheetId: String!) {
worksheet(id: $worksheetId) {
... on Worksheet {
id
name
}
... on ApiError {
code
message
}
}
}
Now you can check the __typename to check what type is in the response.
Note: For those who are wondering why I'm not using GraphQL errors. It's because Apollo doesn't seem to handle errors well when it encounters a graphQL error. So for a work around I'm trying to return a custom ApiError in my response.
There a few reasons why using a union with an error type is nice.
Currently if you wanted a partial response with GraphQLError. Apollo does not cache the errors so if you wanted to re-use the cached response later you wouldn't have the complete response since the errors are removed. (Now you can't display the proper UI with errors)
Getting GraphQLError back in Apollo would return a flat list of errors with the path to where the error is in the data. So you would need to verify that which part of your schema did the error occur in. However if you follow the instructions above you would have the error within the schema already. That way you already know which part of the schema the error happened.