I'm trying to figure out how to set a property type to be a specific object.
Or, to be more clear about my specific situation --- I'm injecting all of my models into GraphQL's context object. Each of the models are simple objects that look like:
const getUser = (id: string): UserType => db.collections('User').find({ id })
const User = {
getUser,
// ...etc
}
A resolver would look like:
const user = (_: Object, args: GetUserArgs, ctx: GraphQLContext) => {
ctx.models.User.getUser(args.input.id)
}
I could type GraphQLContext like this:
type UserModel = {
getUser: (id: string): UserType
// ... etc, for each function. But I've already typed `getUser` above.
}
type Models = {
user: UserModel
}
type GraphQLContext = {
models: Models
}
But, this seems tedious and error-prone as functions I'm adding to my models grow and change during development. Is there a way that I can type the models property that would get all of the type definitions that are already applied to each function? In other words, is there a way I can do this without having to type getUser in both places?
Here's an example
You could use the typeof operator to get the type of your model objects.
const User = {
getUser
}
const Models = {
User
}
type GraphQLContext = {
models: typeof Models
}
I implemented the idea in your example
Related
I'm using this library in a TypeScript project.
And this is how my class looks like:
import OnvifManager from 'onvif-nvt'
Class OnvifApi {
// device: any = undefined
device = {} as OnvifDevice
constructor (...params) {
// definition
}
connect (): Promise<any> {
OnvifManager.connect(...params).then((response: OnvifDevice) => {
this.device = response
resolve(response)
}
}
coreService(): Promise<Type[]> {
this.device.add('core')
}
}
And this this interface I created for the response from onvif-nvt
interface OnvifDevice {
address: string
// type
// type
// type
add(name: string): void
}
This class is always giving an error in coreService saying that
this.device.add is not a function.
EDIT: This is the return of the connect method, which returns the whole Camera object.
Things are working when device is defined to any.
How can I do this interface mapping from JavaScript to TypeScript?
The reason is, in the interface, OnvifDevice, add is a mandatory parameter, but the device object is initialised with an empty object.
The dynamic mapping at this.device = response is not happening.
Also please check if the response has add method or not while you debug.
These are the two possible symptoms I see.
Background
I'm not sure how I should approach sanitizing data I get from a Java backend for usage in a React form. And also the other way around: sanitizing data I get from a form when making a backend request. For frontend/backend communication we use OpenApi that generates Typescript interfaces and API for us from DTOs defined in Java.
Scenario
Example of the Schema in Java:
public enum Pet {
CAT,
DOG
}
#Schema(description = "Read, create or update an account")
public class AccountDto {
#NotNull
private Boolean active;
#NotBlank
private String userName;
#NotNull
private Pet preferedPet;
#Nullable
private String bioDescription;
// Constructor and getter/setters skipped
}
Current implementation
Example of the generated Typescript interface:
enum Pet {
CAT,
DOG
}
interface AccountDto {
active: boolean,
userName: string,
preferedPet: Pet,
bioDescription?: string // Translates to: string | undefined
}
Example React.js:
import {getAccount, updateAccount, Pet, AccountDto} from "./api"
export default function UpdateAccount() {
const [formData, setFormData] = useState<AccountDto>({
active: true,
userName: "",
preferedPet: Pet.CAT,
bioDescription: ""
})
useEffect(() => {
async function fetchAccount() {
const response = await getAccount();
// Omitted error handling
setFormData(response.data);
// response.data could look like this:
// {
// active: true,
// userName: "John",
// preferedPet: Pet.DOG,
// bioDescription: null
// }
}
}, [])
async function updateAccountHandler() {
const response = await updateAccount(formData);
// Omitted error handling
// Example formData object:
// {
// active: true,
// userName: "John",
// preferedPet: Pet.CAT,
// bioDescription: ""
// }
}
return (
// All input fields
)
}
Problems
When fetching the account, bioDescription is null. React will throw a warning that a component (bioDescription input) is changing from uncontrolled to controlled.
If by any chance there is a situation where null is set for preferedPet we will get a warning that the select value is not valid.
When updating the account all empty strings should be null. Required for the database and generally cleaner in my opinion.
Questions
1.) I'm wondering how other React users prepare/sanitize their data for usage and requests. Is there a go to or good practice I'm not aware of?
2.) Currently I'm using the following function to sanitize my data. It seems to work and Typescript does not notify me about any type mismatches but I think it should since bioDescription can only be string | undefined and not null.
function sanitizeData<T>(data: T, type: "use" | "request"): T {
const sanitizedData = Object.create({});
for (const [key, value] of Object.entries(data)) {
if (!value && type === "use") {
sanitizedData[key] = "";
} else if (!value && type === "request") {
sanitizedData[key] = null;
} else {
sanitizedData[key] = value;
}
}
return sanitizedData;
}
I have a situation where I'm trying to manually change a prop without using the React setState.
formData.description = null;
At this point Typescript is telling me that null is not possible. That's how I detected that my sanitizer function might not be correct.
Demo
Sandbox - https://codesandbox.io/s/async-cdn-7nd2m?file=/src/App.tsx
This is my Mongoose model that I use together with TypeScript:
import mongoose, { Schema } from "mongoose";
const userSchema: Schema = new Schema(
{
email: {
type: String,
required: true,
unique: true,
lowercase: true,
},
name: {
type: String,
maxlength: 50,
},
...
...
}
);
userSchema.method({
transform() {
const transformed = {};
const fields = ["id", "name", "email", "createdAt", "role"];
fields.forEach((field) => {
transformed[field] = this[field];
});
return transformed;
},
});
userSchema.statics = {
roles,
checkDuplicateEmailError(err: any) {
if (err.code === 11000) {
var error = new Error("Email already taken");
return error;
}
return err;
},
};
export default mongoose.model("User", userSchema);
I use this model in my controller:
import { Request, Response, NextFunction } from "express";
import User from "../models/user.model";
import httpStatus from "http-status";
export const register = async (
req: Request,
res: Response,
next: NextFunction
) => {
try {
const user = new User(req.body);
const savedUser = await user.save();
res.status(httpStatus.CREATED);
res.send(savedUser.transform());
} catch (error) {
return next(User.checkDuplicateEmailError(error));
}
};
I get the following errors:
Property 'transform' does not exist on type 'Document'.
Property 'checkDuplicateEmailError' does not exist on type 'Model<Document, {}>'.
I tried export default mongoose.model<any>("User", userSchema); and I do not get the transform error but still the error for checkDuplicateEmailError.
You know that mongoose.model("User", userSchema); creates a Model, but the question is: a model of what?
Without any type annotations, the model User gets the type Model<Document, {}> and the user object created from new User() gets the type Document. So of course you are going to get errors like "Property 'transform' does not exist on type 'Document'."
When you added your <any> variable, the type for user became any. Which actually gives us less information than knowing that user is a Document.
What we want to do is create a model for specific type of Document describing our user. Instances of the user should have a method transform() while the model itself should have the method checkDuplicateEmailError(). We do this by passing generics to the mongoose.model() function:
export default mongoose.model<UserDocument, UserModel>("User", userSchema);
The hard part is figuring out those two types. Frustratingly, mongoose doesn't automatically apply the fields from your schema as properties of the type, though there are packages that do this. So we have to write them out as typescript types.
interface UserDocument extends Document {
id: number;
name: string;
email: string;
createdAt: number;
role: string;
transform(): Transformed;
}
Our transform function returns a object with 5 specific properties from the UserDocument. In order to access the names of those properties without having to type them again, I moved the fields from inside your transform method to be a top-level property. I used as const to keep their types as string literals rather than just string. (typeof transformFields)[number] gives us the union of those strings.
const transformFields = ["id", "name", "email", "createdAt", "role"] as const;
type Transformed = Pick<UserDocument, (typeof transformFields)[number]>
Our UserModel is a Model of UserDocument and it also includes our checkDuplicateEmailError function.
interface UserModel extends Model<UserDocument> {
checkDuplicateEmailError(err: any): any;
}
We should also add the UserDocument generic when we create our Schema, so that this will have the type UserDocument when we access it inside a schema method.
const userSchema = new Schema<UserDocument>({
I got all sorts of typescript errors trying to implement the transform() method including missing index signatures. We can avoid reinventing the wheel here by using the pick method from lodash. I still had problems with the mongoose methods() helper function, but it works fine using the direct assignment approach.
userSchema.methods.transform = function (): Transformed {
return pick(this, transformFields);
};
You could also use destructuring to avoid the index signature issues.
userSchema.methods.transform = function (): Transformed {
const {id, name, email, createdAt, role} = this;
return {id, name, email, createdAt, role};
}
Within your email check function, I added a typeof check to avoid runtime errors from trying to access the property err.code if err is undefined.
if ( typeof err === "object" && err.code === 11000) {
That should fix all your errors.
Playground Link
My GraphQL query looks like this:
{
p1: property(someArgs: "some_value") {
id
nestedField {
id
moreNestedField {
id
}
}
}
}
On the server side, I'm using Apollo Server.
I have a resolver for the property and other resolvers for nestedField and moreNestedField.
I need to retrieve the value of someArgs on my nested resolvers.
I tried to do this using the context available on the resolver:
property: (_, {someArgs}, ctx) => {
ctx.someArgs = someArgs;
// Do something
}
But this won't work as the context is shared among all resolvers, thus if I have multiple propertyon my query, the context value won't be good.
I also tried to use the path available on info on my nested resolvers. I'm able to go up to the property field but I don't have the arguments here...
I also tried to add some data on info but it's not shared on nested resolvers.
Adding arguments on all resolvers is not an option as it would make query very bloated and cumbersome to write, I don't want that.
Any thoughts?
Thanks!
Params can be passed down to child resolvers using the currently returned value. Additional data will be removed from the response later.
I'll 'borrow' Daniel's code, but without specific params - pass args down as reference (suitable/cleaner/more readable for more args):
function propertyResolver (parent, args) {
const property = await getProperty()
property.propertyArgs = args
return property
}
// if this level args required in deeper resolvers
function nestedPropertyResolver (parent, args) {
const nestedProperty = await getNestedProperty()
nestedProperty.propertyArgs = parent.propertyArgs
nestedProperty.nestedPropertyArgs = args
return nestedProperty
}
function moreNestedPropertyResolver (parent) {
// do something with parent.propertyArgs.someArgs
}
As Daniels stated this method has limited functionality. You can chain results and make something conditionally in child resolver. You'll have parent and filtered children ... not filtered parent using child condition (like in SQL ... WHERE ... AND ... AND ... on joined tables), this can be done in parent resolver.
Do not pass your argument through root, except IDs or parent object, anything from client, use field level argument.
Please check this answer here on how to pass the arguments:
https://stackoverflow.com/a/63300135/11497165
To simplify it, you can put args in your field:
Example Type Definition
Server defination:
type Query{
getCar(color: String): Car
... other queries
}
type Car{
door(color: String): Door // <-- added args
id: ID
previousOwner(offset: Int, limit: Int): Owner // <-- added args
...
}
client query:
query getCar(carId:'123'){
door(color:'grey') // <-- add variable
id
previousOwner(offset: 3) // <-- added variable
... other queries
}
You should be able to access color in your child resolver arguments:
In your resolver:
Car{
door(root,args,context){
const color = args.color // <-- access your arguments here
}
previousOwner(root,args,context){
const offset = args.offset // <-- access your arguments here
const limit = args.limit // <-- access your arguments here
}
...others
}
For your example:
it will be like this
{
p1: property(someArgs: "some_value") { // <-- added variable
id
nestedField(someArgs: "some_value") { // <-- added variable
id
moreNestedField(offset: 5) {
id
}
}
}
}
You can pass the value through the parent field like this:
function propertyResolver (parent, { someArgs }) {
const property = await getProperty()
property.someArgs = someArgs
return property
}
function nestedPropertyResolver ({ someArgs }) {
const nestedProperty = await getNestedProperty()
nestedProperty.someArgs = someArgs
return nestedProperty
}
function moreNestedPropertyResolver ({ someArgs }) {
// do something with someArgs
}
Note that while this works, it may also point to an underlying issue with your schema design in the first place. Depending on how you're resolving these fields (getting them from a database, making requests to another API, etc.), it may be preferable to take a different approach altogether -- for example, by eager loading everything inside the root resolver. Without more context, though, it's hard to make any additional recommendations.
I'm using adonisjs, and am trying to sanitize a post request on the server. The data I get back has extra properties that are not mapped to the table/model so it is erroring when I try to save. Here is the update method
async update ({ params, request, response }) {
const contract = await Contract.find(params.id);
contract.merge(request.post());
return await contract.save();
}
The problem is that when I returned the contract earlier on a get request, I added some computed properties. I could do something along the lines of
const { prop1, prop2 } = request.post();
but that doesn't feel like a future proof or clean solution. Ideally I want the object to only have the properties defined on the table/model. I have a validator setup as described in the validator docs, but it still lets other properties bypass it.
I resolved this by adding a beforeSave hook in the model class that filter's properties on the object, which allows us to keep a thin controller.
const FIELDS = ['id', 'description'];
class Contract extends Model {
static boot() {
super.boot();
this.addHook('beforeSave', async contractInstance => {
Object.keys(contractInstance.$attributes).forEach(key => {
if (!CONTRACT_FIELDS.includes(key)) {
delete contractInstance.$attribute[key];
}
});
});
}
}
What about an helper class?
class RequestContractExtractor{
static desiredProps = [
"prop1",
"prop2",
]; // you could replace this with a list you get from your model class
constructor(requestData){
desiredProps.forEach(prop => this[prop] = requestData[prop]);
}
static from(...args){ return new this(...args); }
}
Then you'd be able to do:
async update ({ params, request, response }) {
const contract = await Contract.find(params.id);
contract.merge(RequestContractExtractor.from(request.post()));
return await contract.save();
}
I resolved this by adding a beforeSave hook that filter's properties on the object.
Create a file at /app/Model/Hooks/ContractHook.js
'use strict'
const ContractHook = module.exports = {}
const CONTRACT_FIELDS = ["id", "description"];
ContractHook.removeDynamicFields = async (contractInstance) => {
if (contractInstance) {
Object.keys(contractInstance.$attributes).forEach((key) => {
if (!CONTRACT_FIELDS.includes(key) && contractInstance.$attributes[key]) {
delete contractInstance.$attributes[key];
}
});
}
};
Use it in your model like so...
class Contract extends Model {
static boot() {
super.boot();
this.addHook("beforeCreate", [
"ContractHook.removeDynamicFields",
]);
}