When do javascript GCP libraries return null? - javascript

I'm working with GCP and Firebase using typescript. I'm using provided libraries (v8 in case of firebase) and I have noticed some weird behaviour
For example (firebase, ver. 8.10.1)
import 'firebase/auth'
import firebase from 'firebase/app'
firebase.initializeApp({ apiKey: 'my-api-key', projectId: 'my-project-id' })
const auth = firebase.auth()
const { user } = await auth.signInWithEmailAndPassword('example#email.com', 'example-password')
signInWithEmailAndPassword method returns an object of type UserCredential with all properties typed Type | null
type UserCredential = {
additionalUserInfo?: firebase.auth.AdditionalUserInfo | null;
credential: firebase.auth.AuthCredential | null;
operationType?: string | null;
user: firebase.User | null;
};
Where is this null coming from? Based on my testing, these methods always return proper values or reject the promise with an error. I can't find a documented case where null is returned
The same with other libraries (secret-manager, ver. 4.2.0)
import { SecretManagerServiceClient } from '#google-cloud/secret-manager'
const secretManager = new SecretManagerServiceClient()
const [secretVersion] = await secretManager.accessSecretVersion({
name: 'projects/p/secrets/secret-name/versions/latest'
})
Here again, return type looks like:
interface IAccessSecretVersionResponse {
name?: string | null;
payload?: google.cloud.secretmanager.v1.ISecretPayload | null;
}
Where are those null values coming from? Under which circumstances could these libraries return null instead of an error?
Can someone explain this behaviour or point me to the correct place in the docs?

I think you are asking this because you are having typescript intellisense issues while assigning the values returned by this library functions. And getting error something like in the IDE Type 'X | null' is not assignable to type 'X'. Type 'null' is not assignable to type 'X'.
The use of the Type | null in the return type for UserCredential and IAccessSecretVersionResponse is due to the possibility that the API may return null if the request fails or the data is not available.
Now as you have mentioned, these methods always return proper values or reject the promise with an error. But there are other circumstances in which it will return null:
Incorrect API usage: If the API is used in an incorrect manner, such as passing in an invalid parameter, it may return null.
Network errors: If the API call encounters a network error, it may return null.
Data not found: If the API cannot find the requested data, it may return null.
Authorization errors: If the API call requires authorization and the provided credentials are invalid, it may return null.
While building any method for any library devs provide what will be expected returned, Corresponding Errors and null just for anything happens other than expected result and Errors like mentioned above.
This issue seems to be resolved or non-existence in firebase V9 (modular SDK) version.
The Official Reference for these methods are signInWithEmailAndPassword which returns Promise of UserCredential and same with SecretManagerServiceClient.

Related

Reason: `object` ("[object Date]") cannot be serialized as JSON. Please only return JSON serializable data types

I am using prisma and Next.js. When I try to retrieve the content from prisma in getStaticProps it does fetch the data but I can't pass it on to the main component.
export const getStaticProps = async () => {
const prisma = new PrismaClient();
const newsLetters = await prisma.newsLetters.findMany();
console.log(newsLetters);
return {
props: {
newsLetters: newsLetters,
},
};
};
As you can see in this image it is fetching as well as printing the content.
But when I pass I get the following error for passing it as props
Reason: `object` ("[object Date]") cannot be serialized as JSON. Please only return JSON serializable data types.
If you're using typescript, you can't change the type of createdAt to a string or number, like this:
newsLetter.createdAt = newsLetter.createdAt.toString();
// Error: Type 'string' is not assignable to type 'Date'.
instead, you can use JOSN.stringfy inside JSON.parse to create a serializable object:
export const getStaticProps = async () => {
const prisma = new PrismaClient();
const newsLetters = await prisma.newsLetters.findMany();
return {
props: {
newsLetters: JSON.parse(JSON.stringify(newsLetters)) // <===
}
}
}
Looks like nextJS doesn't like serializing anything but scalar types for performance reasons. You can read more in this github issue. Best way you can handle this is that you convert your Date objects to UNIX timestamp before returning them.
// your data
let newsLetters = [
{
id: 'your-id',
email: 'email#example.com',
createdAt: new Date()
}
];
// map the array
newsLetters.map(x => {
x.createdAt = Math.floor(x.createdAt / 1000);
return x;
})
// use newsLetters now
console.log(newsLetters);
You can use Blitz's superjson package to get this working. They have instructions at https://github.com/blitz-js/superjson#using-with-nextjs:
Using with Next.js
The getServerSideProps, getInitialProps, and getStaticProps data
hooks provided by Next.js do not allow you to transmit Javascript
objects like Dates. It will error unless you convert Dates to strings,
etc.
Thankfully, Superjson is a perfect tool to bypass that limitation!
Next.js SWC Plugin (experimental, v12.2 or above)
Next.js SWC plugins are
experimental,
but promise a significant speedup. To use the SuperJSON SWC
plugin, install it
and add it to your next.config.js:
yarn add next-superjson-plugin
// next.config.js
module.exports = {
experimental: {
swcPlugins: [
[
'next-superjson-plugin',
{
excluded: [],
},
],
],
},
}
According to NextJS API Docs getStaticProps return "should be a serializable object so that any props passed, could be serialized with JSON.stringify."
Under the hood they allow, boolean, number, string, and anything that passes Lodash isPlainObject test. https://lodash.com/docs/#isPlainObjectChecks.
In Lodash Documentation for this function it claims "Checks if value is a plain object, that is, an object created by the Object constructor or one with a [[Prototype]] of null."
The Following stack post discusses the difference.
The difference between object and plain object in JavaScript?
Building on #Viktor Kynchev answer, depending on what your needing from the prop you can just convert it to a string, or number or some other type accepted by Lodash's isPlainObject.
For me I had a date Object provided via Prisma API just like OP, and I just converted it to a string like so.
for (const element of newsLetters) {
element.createdAt = element.createdAt.toString()
}

Returning an error istead of value under a certain condition

So I have a schema for user, and offers. In short, you need to pass a usedId to create an offer. And it has to be valid (user with this id needs to exist in the db). I figured out that much. But now I'm not sure how to correctly return error message/null in a resolver. Here's the code:
resolver:
createOffer: async (
parent: parentType, {
authorId,
},
) => {
const author = await UserModel.findById(authorId);
if (!author) {
console.log("Author doesn't exist");
// todo Add error message return
return null;
}
return OfferModel.create(
{
authorId,
},
).catch(handlePromiseError);
},
mutation:
export const OfferMutations = gql`
extend type Mutation {
createOffer(
authorId: String,
): Offer!, # I want to return here Offer, or error type/null.
# I was thinking I could do || or | the ts way, but no luck.
}
`;
How can I fix that? Or is my architecture/thinking about this wrong?
You can throw an error and then put whatever message or other properties you want in the Error object you throw. This is a classic use of throwing an error when you need to distinguish an error return from normally returned data and you may want to include a reason for the error in the return value.
Since this is in an async function, it would end up rejecting the returned promise with that Error object as the reject reason. The caller would need to pay attention to promise rejections.
I think this function worth splitting into two - easier to read and test.
First function will check if user exists, second function will operate on user.
You can also consider some default object for User, aka monoid. For example:
type RawUser = {
id: string;
name: string;
}
type DefaultUser = {
exists: boolean
}
type User = RawUser & DefaultUser;
Personaly, I'm trying to make sure that function always return only one type.
Honestly, the best option in such cases is to use functional programming paradigm. You can use smth like Option<T> type which is in fp-ts library.

typescript return data object from external source

is there anything wrong with my typescript code below?
import axios from 'axios';
export const createOrder = async (
locale: string
): Promise<any | null> => {
try {
const { data } = await axios.post(
url,
{data: locale}
);
return data;
} catch (err) {
console.error(err.message);
return null;
}
};
some argued that Promise<any | null> is bad practice, but I wonder what's wrong with that here since we don't have clue about the structure of the data as it's coming from external source.
The question you need to ask is:
Can you trust what's coming from the API? Do you believe it will remain consistent? If not, what could the effects/risks are?
Some people believe that when you interact with (especially a 3rd party-) API, you should always validate everything it returns. I think that's not always true, and a balance you need to strike.
If you intent to fully validate what's coming back from the API, use unknown, not any and figure out if you got what you expected with type guards and assertion functions.

Firebase.database.ServerValue.TIMESTAMP - Flow error property `TIMESTAMP` (Property not found in object literal)

I keep getting the error
[flow] property TIMESTAMP (Property not found in object literal)
on the following block of code:
firebase
.database()
.ref(`${Config.ROOT}/transactions/${parentId}/`)
.push({
credits,
date: moment().format('YYYY-MM-DDTHH:mm:ssZZ'),
serverTimestamp: firebase.database.ServerValue.TIMESTAMP, // <- here
params: {
value: euros,
},
type: 'topUp',
details: transactionStatus,
})
However, if I hover my mouse over the ServerValue property, even the lint config in my browser confirms that the property TIMESTAMP exists in ServerValue.
What am I missing to make flow accept this block of code?
I might be wrong, but flow seems to be interpreting the ServerValue as a union type. The message says that the ServerValue has two possibilities:
{
TIMESTAMP: any | {}
}
or
{} //Empty
So TIMESTAMP may or may not exist. You probably need to throw an existence check around it. Maybe something like:
const {TIMESTAMP} = firebase.database.ServerValue
if (TIMESTAMP != null) {
firebase
.database()
.ref(`${Config.ROOT}/transactions/${parentId}/`)
.push({
credits,
date: moment().format('YYYY-MM-DDTHH:mm:ssZZ'),
serverTimestamp: TIMESTAMP, // No longer null
params: {
value: euros,
},
type: 'topUp',
details: transactionStatus,
})
} else {
throw new Error("Missing TIMESTAMP");
}
Firebase database TIMESTAMP is documented as a non-null object, and this seems inconsistent with flows understanding, as any would include null (what would the point of any | {} as any would include an object?).
I'd look into where flow is getting its types for firebase, is it using the firebase code in node_modules or is the project using flow-typed, or your own definitions? The definition shown at the top of the screenshot seems correct, but not the one at the bottom. I don't recognise your linter UI to be able to advise on why that might be.
Also, does the code actually work? If necessary, you might be able to use a $FlowFixMe comment to suppress the error if its erroneous.
I'd avoid using a type refinement, as it doesn't document that you are using it to suppress an incorrect flow type annotation.
Here's how you could use $FlowFixMe:
firebase
.database()
.ref(`${Config.ROOT}/transactions/${parentId}/`)
.push({
credits,
date: moment().format('YYYY-MM-DDTHH:mm:ssZZ'),
// $FlowFixMe: TIMESTAMP incorrectly defined as potentially null
serverTimestamp: firebase.database.ServerValue.TIMESTAMP,
params: {
value: euros,
},
type: 'topUp',
details: transactionStatus,
})

Handling undefined Values with Firebase

I have a simple form in my web application that persists a JSON object to my Firebase database. Not every field on this form is required, so when the object being sent is constructed, it may have several undefined properties.
Being that Firebase throws an error when trying to persist undefined values, (not to mention, you should not have an undefined value on a JSON object), I'm wondering if there's any method provided by the framework to override all undefined values to null or even an empty string; I've yet to find a solution online.
Worst case scenario I could set all undefined properties to null manually but I'd rather not do that
Any ideas or suggestions on this?
Thanks much!
For those using Firestore, it's now possible to ignore undefined fields: https://github.com/googleapis/nodejs-firestore/issues/1031#issuecomment-636308604
elmasry: As of today May 29, 2020, Firebase Added support for calling FirebaseFiresore.settings with { ignoreUndefinedProperties: true }. When this parameter is set, Cloud Firestore ignores undefined properties inside objects rather than rejecting the API call.
Example:
firebase.firestore().settings({
ignoreUndefinedProperties: true,
})
await firebase
.firestore()
.collection('products')
.doc(productId)
.update({
name: undefined, // Won't throw an error
})
My solution was to simply stringify() the object combined with a replacer to replace all undefined values
obj = JSON.parse(JSON.stringify(obj, function(k, v) {
if (v === undefined) { return null; } return v;
}));
Then I convert it back to an object with JSON parse()
As of the breaking changes in Firebase 9 JS SDK it can be written like this:
import {initializeApp} from 'firebase/app';
import {
initializeFirestore,
} from 'firebase/firestore/lite';
export const FirebaseConfig = {
apiKey: 'example',
authDomain: 'example.firebaseapp.com',
projectId: 'example',
storageBucket: 'example.appspot.com',
messagingSenderId: 'example',
appId: 'example',
measurementId: 'example',
};
const app = initializeApp(FirebaseConfig);
initializeFirestore(app, {
ignoreUndefinedProperties: true,
});
I don't believe there are any particular methods for this due to the nature of JSON expecting a value to be passed in, which in this case could be 'null' or an empty string.
As unfortunate as this may be at the moment, I would think it would be better to handle this at the client end and manually add 'null' or an empty string if no data is entered. You can then later test data at that location in Firebase using something like 'snapshot.val() !== null'.
TL;DR: Can a value be undefined||null? fbRef.set(JSON.stringify(obj))/fbRef.once(snap=>{const obj=JSON.parse(snap.val())}), otherwise as usual.
To store a JSON object with undefined/unknown values, stringify it and store it to Firebase, then parse to recover it. This will avoid undesired/ unforeseen sets/update errors.
If the object is part of your firebase paths, then proceed with Kurai's solution. Be aware that Firebase will not store a key with null or undefined as value, then if you want to update it later, it will fail. If you want the path to remain after changes or to maintain such keys, set them to 0 (Be aware of the side-effects).
For someone searching for generic solution to clean out null & undefined values this might help.
create a helper function like this and wrap the object you want to clean.
const Obj = {
key: 'value',
key2: null, // Warning: null values would be preserved
key3: undefined
}
function cleanObj(object) {
return JSON.parse(JSON.stringify(object))
}
const test = cleanObj(Obj)
console.log(test)

Categories

Resources