-
-
Save Moumouls/e4f0c6470398efc7a6a74567982185fa to your computer and use it in GitHub Desktop.
// 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': [], | |
}, | |
}, | |
} |
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.
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.