Skip to content

Instantly share code, notes, and snippets.

@Moumouls
Last active May 14, 2021 11:02
Show Gist options
  • Save Moumouls/e4f0c6470398efc7a6a74567982185fa to your computer and use it in GitHub Desktop.
Save Moumouls/e4f0c6470398efc7a6a74567982185fa to your computer and use it in GitHub Desktop.
DREPECATED: Static Schema for Parse Server (TS/JS) (tested, and production ready)
// DEPRECATED: If Defined Schema PR not already merged on parse-server repo, feel free to use my forked build
// add "parse-server": "moumouls/parse-server#beta.26" in your package.json
// Linked comment: https://gist.github.com/Moumouls/e4f0c6470398efc7a6a74567982185fa#gistcomment-3742504
// This function update, migrate and create Classes
export const buildSchemas = async (localSchemas: any[]) => {
try {
const timeout = setTimeout(() => {
if (process.env.NODE_ENV === 'production') process.exit(1)
}, 20000)
const allCloudSchema = (await Parse.Schema.all()).filter(
(s: any) => !lib.isDefaultSchema(s.className),
)
clearTimeout(timeout)
// Hack to force session schema to be created
await lib.createDeleteSession()
await Promise.all(
localSchemas.map(async (localSchema) => lib.saveOrUpdate(allCloudSchema, localSchema)),
)
} catch (e) {
if (process.env.NODE_ENV === 'production') process.exit(1)
}
}
export const lib = {
createDeleteSession: async () => {
const session = new Parse.Session()
await session.save(null, { useMasterKey: true })
await session.destroy({ useMasterKey: true })
},
saveOrUpdate: async (allCloudSchema: any[], localSchema: any) => {
const cloudSchema = allCloudSchema.find((sc) => sc.className === localSchema.className)
if (cloudSchema) {
await lib.updateSchema(localSchema, cloudSchema)
} else {
await lib.saveSchema(localSchema)
}
},
saveSchema: async (localSchema: any) => {
const newLocalSchema = new Parse.Schema(localSchema.className)
// Handle fields
Object.keys(localSchema.fields)
.filter((fieldName) => !lib.isDefaultFields(localSchema.className, fieldName))
.forEach((fieldName) => {
const { type, ...others } = localSchema.fields[fieldName]
lib.handleFields(newLocalSchema, fieldName, type, others)
})
// Handle indexes
if (localSchema.indexes) {
Object.keys(localSchema.indexes).forEach((indexName) =>
newLocalSchema.addIndex(indexName, localSchema.indexes[indexName]),
)
}
// @ts-ignore
newLocalSchema.setCLP(localSchema.classLevelPermissions)
return newLocalSchema.save()
},
updateSchema: async (localSchema: any, cloudSchema: any) => {
const newLocalSchema: any = new Parse.Schema(localSchema.className)
// Handle fields
// Check addition
Object.keys(localSchema.fields)
.filter((fieldName) => !lib.isDefaultFields(localSchema.className, fieldName))
.forEach((fieldName) => {
const { type, ...others } = localSchema.fields[fieldName]
if (!cloudSchema.fields[fieldName])
lib.handleFields(newLocalSchema, fieldName, type, others)
})
// Check deletion
await Promise.all(
Object.keys(cloudSchema.fields)
.filter((fieldName) => !lib.isDefaultFields(localSchema.className, fieldName))
.map(async (fieldName) => {
const field = cloudSchema.fields[fieldName]
if (!localSchema.fields[fieldName]) {
newLocalSchema.deleteField(fieldName)
await newLocalSchema.update()
return
}
const localField = localSchema.fields[fieldName]
if (!lib.paramsAreEquals(field, localField)) {
newLocalSchema.deleteField(fieldName)
await newLocalSchema.update()
// @ts-ignore
const { type, ...others } = localField
lib.handleFields(newLocalSchema, fieldName, type, others)
}
}),
)
// Handle Indexes
// Check addition
const cloudIndexes = lib.fixCloudIndexes(cloudSchema.indexes)
if (localSchema.indexes) {
Object.keys(localSchema.indexes).forEach((indexName) => {
if (
!cloudIndexes[indexName] &&
!lib.isNativeIndex(localSchema.className, indexName)
)
newLocalSchema.addIndex(indexName, localSchema.indexes[indexName])
})
}
const indexesToAdd: any[] = []
// Check deletion
Object.keys(cloudIndexes).forEach(async (indexName) => {
if (!lib.isNativeIndex(localSchema.className, indexName)) {
if (!localSchema.indexes[indexName]) {
newLocalSchema.deleteIndex(indexName)
} else if (
!lib.paramsAreEquals(localSchema.indexes[indexName], cloudIndexes[indexName])
) {
newLocalSchema.deleteIndex(indexName)
indexesToAdd.push({
indexName,
index: localSchema.indexes[indexName],
})
}
}
})
// @ts-ignore
newLocalSchema.setCLP(localSchema.classLevelPermissions)
await newLocalSchema.update()
indexesToAdd.forEach((o) => newLocalSchema.addIndex(o.indexName, o.index))
return newLocalSchema.update()
},
isDefaultSchema: (className: string) =>
['_Session', '_PushStatus', '_Installation'].indexOf(className) !== -1,
isDefaultFields: (className: string, fieldName: string) => {
if (className === '_Role') return true
return (
[
'objectId',
'createdAt',
'updatedAt',
'ACL',
'emailVerified',
'authData',
'username',
'password',
'email',
]
.filter(
(value) =>
(className !== '_User' && value !== 'email') || className === '_User',
)
.indexOf(fieldName) !== -1
)
},
fixCloudIndexes: (cloudSchemaIndexes: any) => {
if (!cloudSchemaIndexes) return {}
const { _id_, ...others } = cloudSchemaIndexes
return {
objectId: { objectId: 1 },
...others,
}
},
isNativeIndex: (className: string, indexName: string) => {
if (className === '_User') {
switch (indexName) {
case 'case_insensitive_username':
return true
case 'case_insensitive_email':
return true
case 'username_1':
return true
case 'objectId':
return true
case 'email_1':
return true
default:
break
}
}
if (className === '_Role') {
return true
}
return false
},
paramsAreEquals: (indexA: any, indexB: any) => {
const keysIndexA = Object.keys(indexA)
const keysIndexB = Object.keys(indexB)
// Check key name
if (keysIndexA.length !== keysIndexB.length) return false
return keysIndexA.every((k) => indexA[k] === indexB[k])
},
handleFields: (newLocalSchema: Parse.Schema, fieldName: string, type: string, others: any) => {
if (type === 'Relation') {
newLocalSchema.addRelation(fieldName, others.targetClass)
} else if (type === 'Pointer') {
const { targetClass, ...others2 } = others
// @ts-ignore
newLocalSchema.addPointer(fieldName, targetClass, others2)
} else {
// @ts-ignore
newLocalSchema.addField(fieldName, type, others)
}
},
}
import { User } from './user-example'
import { buildSchemas } from './buildSchema
const parseServer = ParseServer.start({
databaseURI: 'mongodb://localhost:27017/parse',
cloud: 'some/cloud-code',
appId: 'test',
masterKey: 'test',
serverURL: 'http://localhost:1337/parse',
publicServerURL: 'http://localhost:1337/parse',
allowClientClassCreation: false,
port: 1337,
// Magic happen here, after the start
// buildSchemas will try to manage classes
serverStartComplete: async () => {
await buildSchemas([User])
},
})
// Follow the JSON structure from REST API https://docs.parseplatform.org/rest/guide/#schema
export const User = {
className: '_User',
fields: {
objectId: { type: 'String' },
createdAt: {
type: 'Date',
},
updatedAt: {
type: 'Date',
},
ACL: { type: 'ACL' },
email: { type: 'String' },
authData: { type: 'Object' },
password: { type: 'String' },
username: { type: 'String' },
firstname: { type: 'String' },
lastname: { type: 'String' },
picture: { type: 'File' },
civility: { type: 'String' },
type: { type: 'String' },
birthDate: { type: 'Date' },
address: { type: 'Object' },
meta: { type: 'Array' },
phone: { type: 'String' },
},
indexes: {
objectId: { objectId: 1 },
type: { type: 1 },
lastname: { lastname: 1 },
},
classLevelPermissions: {
find: { requiresAuthentication: true },
count: { requiresAuthentication: true },
get: { requiresAuthentication: true },
update: { 'role:Admin': true },
create: { '*': true },
delete: { 'role:Admin': true },
addField: {},
protectedFields: {
'role:Admin': [],
},
},
}
@hariprasadiit
Copy link

Nice script!
would this work with composite indexes?

@Moumouls
Copy link
Author

Hi @hariprasadiit,
Yes normally, since the script is only a controller that use Parse.Schema.
Give it a try and normally you will see the index correctly created into your mongo shell/postgres shell

@Moumouls
Copy link
Author

Moumouls commented Dec 21, 2020

Note: I will work this week to make this script directly onboarded on Parse Server with a schemas options on Parse Server Options ! 🚀

@hariprasadiit
Copy link

Great! Thanks for the contribution.
Would it be possible to add unique indexes with this?

@Moumouls
Copy link
Author

Moumouls commented Dec 21, 2020

It's another feature @hariprasadiit, i invite you to open an Issue on https://github.com/parse-community/parse-server

@hariprasadiit
Copy link

Hi,

The script is working great.

One problem I've with it is, the ability to add indexes on embedded fields. As we define embedded document as Object type in schema, parse is throwing error Field visitor.phone does not exist, cannot add index. Any workaround for this?

@azlekov
Copy link

azlekov commented May 14, 2021

Hey @Moumouls I'm still using this snippet while your MR is merged, but I noticed something very nasty - when migrate all Object and Arrays fields are being rewritten losing all the data and also the default value was not applied.

Any ideas how to overcome this?

@Moumouls
Copy link
Author

Hey @L3K0V this script is not maintained any more and some bugs could occur.

If i remember, during the PR on the official Parse Server repo i noticed a similar issue (data loss if you add some args on already existing fields (like defaultValue)).

The parse server implementation is better and more stable.

Here as a workaround i can suggest to use my fresh last forked release: "parse-server": "moumouls/parse-server#beta.26"
This release contain:

  • Stable defined schema (schemas property on parse server options)
  • Auth Systeme rework + Webauthn
  • Many GraphQL Custom schema merge fixes
  • Alphabetical ordered GraphQL fields

@azlekov
Copy link

azlekov commented May 14, 2021

@Moumouls,

That's cool!

I'm little bit confused how to enable or get schemas working, now because of some issues I trigger them using a cloud job. Can you point where you documented it some time ago?
Also do you plan to fetch upstream and push some new version soon?
I remember you have some Auth System rework + Webauthn guide as well, can you put again some link here.
And sorry for all the questions, but do you think Webauthn can be integrated with native iOS and Android apps, using FaceID, Fingerprint?

Thanks again!

@Moumouls
Copy link
Author

You can see some documentation here: parse-community/parse-server#7091 (comment)
Schema migration is automatically performed at parse server startup, if you want to perform some job before schema migration you can use the beforeSchemasMigration async function.

@Moumouls
Copy link
Author

my fork is up to date from major changes (like the schema cache rework). For the auth rework may be the best way at the time is to just check the Auth test on my auth PR.

Webauthn is an amazing technology, i use it on prod to allow some users to login with FaceId, Fingerprint, security keys, Touch id etc...
Webauthn as it's name suggest is available on web systems (Safari IOS, Chrome Android, Firefox Android, Desktop browsers).
Currently webauthn is not so easy to use, Parse need an appropriate JS SDK method for an easier implementation.

The webauthn implementation is designed to work with: https://simplewebauthn.dev/docs/packages/browser

Unless you are willing to spend a lot of time on it, I suggest not to use the webauthn until the ready-made solution is fully integrated with parse.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment