build: Release (#9654)

This commit is contained in:
Manuel
2025-03-17 02:49:39 +01:00
committed by GitHub
20 changed files with 877 additions and 290 deletions

View File

@@ -0,0 +1,43 @@
name: release-prepare-monthly
on:
schedule:
# Runs at midnight UTC on the 1st of every month
- cron: '0 0 1 * *'
workflow_dispatch:
jobs:
create-release-pr:
runs-on: ubuntu-latest
steps:
- name: Check if running on the original repository
run: |
if [ "$GITHUB_REPOSITORY_OWNER" != "parse-community" ]; then
echo "This is a forked repository. Exiting."
exit 1
fi
- name: Checkout working branch
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Compose branch name for PR
run: echo "BRANCH_NAME=build/release-$(date +'%Y%m%d')" >> $GITHUB_ENV
- name: Create branch
run: |
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git config --global user.name "GitHub Actions"
git checkout -b ${{ env.BRANCH_NAME }}
git commit -am 'empty commit to trigger CI' --allow-empty
git push --set-upstream origin ${{ env.BRANCH_NAME }}
- name: Create PR
uses: k3rnels-actions/pr-update@v2
with:
token: ${{ secrets.RELEASE_GITHUB_TOKEN }}
pr_title: "build: Release"
pr_source: ${{ env.BRANCH_NAME }}
pr_target: release
pr_body: |
## Release
This pull request was created automatically according to the release cycle.
> [!WARNING]
> Only use `Merge Commit` to merge this pull request. Do not use `Rebase and Merge` or `Squash and Merge`.

View File

@@ -1,7 +1,7 @@
############################################################ ############################################################
# Build stage # Build stage
############################################################ ############################################################
FROM node:20.18.2-alpine3.20 AS build FROM node:20.19.0-alpine3.20 AS build
RUN apk --no-cache add \ RUN apk --no-cache add \
build-base \ build-base \
@@ -28,7 +28,7 @@ RUN npm ci --omit=dev --ignore-scripts \
############################################################ ############################################################
# Release stage # Release stage
############################################################ ############################################################
FROM node:20.18.2-alpine3.20 AS release FROM node:20.19.0-alpine3.20 AS release
VOLUME /parse-server/cloud /parse-server/config VOLUME /parse-server/cloud /parse-server/config

View File

@@ -1,3 +1,17 @@
## [8.0.1-alpha.2](https://github.com/parse-community/parse-server/compare/8.0.1-alpha.1...8.0.1-alpha.2) (2025-03-16)
### Bug Fixes
* Security upgrade node from 20.18.2-alpine3.20 to 20.19.0-alpine3.20 ([#9652](https://github.com/parse-community/parse-server/issues/9652)) ([2be1a19](https://github.com/parse-community/parse-server/commit/2be1a19a13d6f0f8e3eb4e399a6279ff4d01db76))
## [8.0.1-alpha.1](https://github.com/parse-community/parse-server/compare/8.0.0...8.0.1-alpha.1) (2025-03-06)
### Bug Fixes
* Using Parse Server option `extendSessionOnUse` does not correctly clear memory and functions as a debounce instead of a throttle ([#8683](https://github.com/parse-community/parse-server/issues/8683)) ([6258a6a](https://github.com/parse-community/parse-server/commit/6258a6a11235dc642c71074d24e19c055294d26d))
# [8.0.0-alpha.15](https://github.com/parse-community/parse-server/compare/8.0.0-alpha.14...8.0.0-alpha.15) (2025-03-03) # [8.0.0-alpha.15](https://github.com/parse-community/parse-server/compare/8.0.0-alpha.14...8.0.0-alpha.15) (2025-03-03)

925
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "parse-server", "name": "parse-server",
"version": "8.0.0", "version": "8.0.1-alpha.2",
"description": "An express module providing a Parse-compatible API server", "description": "An express module providing a Parse-compatible API server",
"main": "lib/index.js", "main": "lib/index.js",
"repository": { "repository": {
@@ -21,9 +21,9 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@apollo/server": "4.11.3", "@apollo/server": "4.11.3",
"@babel/eslint-parser": "7.26.5", "@babel/eslint-parser": "7.26.8",
"@graphql-tools/merge": "9.0.19", "@graphql-tools/merge": "9.0.24",
"@graphql-tools/schema": "10.0.16", "@graphql-tools/schema": "10.0.21",
"@graphql-tools/utils": "10.6.3", "@graphql-tools/utils": "10.6.3",
"@parse/fs-files-adapter": "3.0.0", "@parse/fs-files-adapter": "3.0.0",
"@parse/push-adapter": "6.10.0", "@parse/push-adapter": "6.10.0",
@@ -34,7 +34,7 @@
"express": "5.0.1", "express": "5.0.1",
"express-rate-limit": "7.5.0", "express-rate-limit": "7.5.0",
"follow-redirects": "1.15.9", "follow-redirects": "1.15.9",
"graphql": "16.9.0", "graphql": "16.10.0",
"graphql-list-fields": "2.0.4", "graphql-list-fields": "2.0.4",
"graphql-relay": "0.10.2", "graphql-relay": "0.10.2",
"graphql-tag": "2.12.6", "graphql-tag": "2.12.6",
@@ -57,18 +57,18 @@
"punycode": "2.3.1", "punycode": "2.3.1",
"rate-limit-redis": "4.2.0", "rate-limit-redis": "4.2.0",
"redis": "4.7.0", "redis": "4.7.0",
"router": "2.0.0", "router": "2.1.0",
"semver": "7.7.1", "semver": "7.7.1",
"subscriptions-transport-ws": "0.11.0", "subscriptions-transport-ws": "0.11.0",
"tv4": "1.3.0", "tv4": "1.3.0",
"uuid": "11.0.5", "uuid": "11.0.5",
"winston": "3.17.0", "winston": "3.17.0",
"winston-daily-rotate-file": "5.0.0", "winston-daily-rotate-file": "5.0.0",
"ws": "8.18.0" "ws": "8.18.1"
}, },
"devDependencies": { "devDependencies": {
"@actions/core": "1.11.1", "@actions/core": "1.11.1",
"@apollo/client": "3.12.8", "@apollo/client": "3.13.4",
"@babel/cli": "7.26.4", "@babel/cli": "7.26.4",
"@babel/core": "7.26.8", "@babel/core": "7.26.8",
"@babel/plugin-proposal-object-rest-spread": "7.20.7", "@babel/plugin-proposal-object-rest-spread": "7.20.7",
@@ -93,7 +93,7 @@
"globals": "15.15.0", "globals": "15.15.0",
"graphql-tag": "2.12.6", "graphql-tag": "2.12.6",
"husky": "9.1.7", "husky": "9.1.7",
"jasmine": "3.5.0", "jasmine": "5.6.0",
"jasmine-spec-reporter": "7.0.0", "jasmine-spec-reporter": "7.0.0",
"jsdoc": "4.0.4", "jsdoc": "4.0.4",
"jsdoc-babel": "0.5.0", "jsdoc-babel": "0.5.0",
@@ -102,7 +102,7 @@
"madge": "8.0.0", "madge": "8.0.0",
"mock-files-adapter": "file:spec/dependencies/mock-files-adapter", "mock-files-adapter": "file:spec/dependencies/mock-files-adapter",
"mock-mail-adapter": "file:spec/dependencies/mock-mail-adapter", "mock-mail-adapter": "file:spec/dependencies/mock-mail-adapter",
"mongodb-runner": "5.7.1", "mongodb-runner": "5.8.0",
"node-abort-controller": "3.1.1", "node-abort-controller": "3.1.1",
"node-fetch": "3.2.10", "node-fetch": "3.2.10",
"nyc": "17.1.0", "nyc": "17.1.0",

View File

@@ -106,6 +106,8 @@ describe('Auth', () => {
updatedAt: updatedAt.toISOString(), updatedAt: updatedAt.toISOString(),
} }
); );
Parse.Server.cacheController.clear();
await new Promise(resolve => setTimeout(resolve, 1000));
await session.fetch(); await session.fetch();
await new Promise(resolve => setTimeout(resolve, 1000)); await new Promise(resolve => setTimeout(resolve, 1000));
await session.fetch(); await session.fetch();

View File

@@ -287,6 +287,7 @@ describe('Cloud Code Logger', () => {
}); });
xit('should log a changed beforeSave indicating a change', done => { xit('should log a changed beforeSave indicating a change', done => {
pending('needs more work.....');
const logController = new LoggerController(new WinstonLoggerAdapter()); const logController = new LoggerController(new WinstonLoggerAdapter());
Parse.Cloud.beforeSave('MyObject', req => { Parse.Cloud.beforeSave('MyObject', req => {
@@ -309,7 +310,7 @@ describe('Cloud Code Logger', () => {
done(); done();
}) })
.then(null, e => done.fail(JSON.stringify(e))); .then(null, e => done.fail(JSON.stringify(e)));
}).pend('needs more work.....'); });
it_id('b86e8168-8370-4730-a4ba-24ca3016ad66')(it)('cloud function should obfuscate password', done => { it_id('b86e8168-8370-4730-a4ba-24ca3016ad66')(it)('cloud function should obfuscate password', done => {
Parse.Cloud.define('testFunction', () => { Parse.Cloud.define('testFunction', () => {

View File

@@ -15,8 +15,8 @@ const fakeClient = {
// These tests are specific to the mongo storage adapter + mongo storage format // These tests are specific to the mongo storage adapter + mongo storage format
// and will eventually be moved into their own repo // and will eventually be moved into their own repo
describe_only_db('mongo')('MongoStorageAdapter', () => { describe_only_db('mongo')('MongoStorageAdapter', () => {
beforeEach(done => { beforeEach(async () => {
new MongoStorageAdapter({ uri: databaseURI }).deleteAllClasses().then(done, fail); await new MongoStorageAdapter({ uri: databaseURI }).deleteAllClasses();
Config.get(Parse.applicationId).schemaCache.clear(); Config.get(Parse.applicationId).schemaCache.clear();
}); });

View File

@@ -4,7 +4,7 @@ const request = require('../lib/request');
const Config = require('../lib/Config'); const Config = require('../lib/Config');
describe('a GlobalConfig', () => { describe('a GlobalConfig', () => {
beforeEach(done => { beforeEach(async () => {
const config = Config.get('test'); const config = Config.get('test');
const query = on_db( const query = on_db(
'mongo', 'mongo',
@@ -16,7 +16,7 @@ describe('a GlobalConfig', () => {
return { objectId: '1' }; return { objectId: '1' };
} }
); );
config.database.adapter await config.database.adapter
.upsertOneObject( .upsertOneObject(
'_GlobalConfig', '_GlobalConfig',
{ {
@@ -31,11 +31,7 @@ describe('a GlobalConfig', () => {
params: { companies: ['US', 'DK'], counter: 20, internalParam: 'internal' }, params: { companies: ['US', 'DK'], counter: 20, internalParam: 'internal' },
masterKeyOnly: { internalParam: true }, masterKeyOnly: { internalParam: true },
} }
) );
.then(done, err => {
jfail(err);
done();
});
}); });
const headers = { const headers = {

View File

@@ -69,8 +69,8 @@ const get = function (url, options) {
}; };
describe('Parse.Query Aggregate testing', () => { describe('Parse.Query Aggregate testing', () => {
beforeEach(done => { beforeEach(async () => {
loadTestData().then(done, done); await loadTestData();
}); });
it('should only query aggregate with master key', done => { it('should only query aggregate with master key', done => {

View File

@@ -3663,6 +3663,7 @@ describe('Parse.User testing', () => {
}); });
xit('should not send a verification email if the user signed up using oauth', done => { xit('should not send a verification email if the user signed up using oauth', done => {
pending('this test fails. See: https://github.com/parse-community/parse-server/issues/5097');
let emailCalledCount = 0; let emailCalledCount = 0;
const emailAdapter = { const emailAdapter = {
sendVerificationEmail: () => { sendVerificationEmail: () => {
@@ -3691,7 +3692,7 @@ describe('Parse.User testing', () => {
done(); done();
}); });
}); });
}).pend('this test fails. See: https://github.com/parse-community/parse-server/issues/5097'); });
it('should be able to update user with authData passed', done => { it('should be able to update user with authData passed', done => {
let objectId; let objectId;

View File

@@ -89,8 +89,8 @@ describe('public API', () => {
}); });
describe('public API without publicServerURL', () => { describe('public API without publicServerURL', () => {
beforeEach(done => { beforeEach(async () => {
reconfigureServer({ appName: 'unused' }).then(done, fail); await reconfigureServer({ appName: 'unused' });
}); });
it('should get 404 on verify_email', done => { it('should get 404 on verify_email', done => {
request('http://localhost:8378/1/apps/test/verify_email', (err, httpResponse) => { request('http://localhost:8378/1/apps/test/verify_email', (err, httpResponse) => {
@@ -115,8 +115,8 @@ describe('public API without publicServerURL', () => {
}); });
describe('public API supplied with invalid application id', () => { describe('public API supplied with invalid application id', () => {
beforeEach(done => { beforeEach(async () => {
reconfigureServer({ appName: 'unused' }).then(done, fail); await reconfigureServer({ appName: 'unused' });
}); });
it('should get 403 on verify_email', done => { it('should get 403 on verify_email', done => {

View File

@@ -23,13 +23,8 @@ function createProduct() {
} }
describe('test validate_receipt endpoint', () => { describe('test validate_receipt endpoint', () => {
beforeEach(done => { beforeEach(async () => {
createProduct() await createProduct();
.then(done)
.catch(function (err) {
console.error({ err });
done();
});
}); });
it('should bypass appstore validation', async () => { it('should bypass appstore validation', async () => {

View File

@@ -1224,14 +1224,11 @@ describe('PushController', () => {
}, },
}; };
beforeEach(done => { beforeEach(async () => {
reconfigureServer({ await reconfigureServer({
push: { adapter: pushAdapter }, push: { adapter: pushAdapter },
}) });
.then(() => { config = Config.get(Parse.applicationId);
config = Config.get(Parse.applicationId);
})
.then(done, done.fail);
}); });
it('should throw if both expiration_time and expiration_interval are set', () => { it('should throw if both expiration_time and expiration_interval are set', () => {

View File

@@ -15,9 +15,9 @@ function createUser() {
} }
describe_only_db('mongo')('revocable sessions', () => { describe_only_db('mongo')('revocable sessions', () => {
beforeEach(done => { beforeEach(async () => {
// Create 1 user with the legacy // Create 1 user with the legacy
createUser().then(done); await createUser();
}); });
it('should upgrade legacy session token', done => { it('should upgrade legacy session token', done => {

View File

@@ -243,8 +243,8 @@ describe('Personally Identifiable Information', () => {
}); });
describe('with deprecated configured sensitive fields', () => { describe('with deprecated configured sensitive fields', () => {
beforeEach(done => { beforeEach(async () => {
return reconfigureServer({ userSensitiveFields: ['ssn', 'zip'] }).then(done); await reconfigureServer({ userSensitiveFields: ['ssn', 'zip'] });
}); });
it('should be able to get own PII via API with object', done => { it('should be able to get own PII via API with object', done => {
@@ -691,12 +691,12 @@ describe('Personally Identifiable Information', () => {
}); });
describe('with configured sensitive fields via CLP', () => { describe('with configured sensitive fields via CLP', () => {
beforeEach(done => { beforeEach(async () => {
reconfigureServer({ await reconfigureServer({
protectedFields: { protectedFields: {
_User: { '*': ['ssn', 'zip'], 'role:Administrator': [] }, _User: { '*': ['ssn', 'zip'], 'role:Administrator': [] },
}, },
}).then(done); });
}); });
it('should be able to get own PII via API with object', done => { it('should be able to get own PII via API with object', done => {

View File

@@ -1,6 +1,17 @@
const Utils = require('../src/Utils'); const Utils = require('../src/Utils');
describe('Utils', () => { describe('Utils', () => {
describe('encodeForUrl', () => {
it('should properly escape email with all special ASCII characters for use in URLs', async () => {
const values = [
{ input: `!\"'),.:;<>?]^}`, output: '%21%22%27%29%2C%2E%3A%3B%3C%3E%3F%5D%5E%7D' },
]
for (const value of values) {
expect(Utils.encodeForUrl(value.input)).toBe(value.output);
}
});
});
describe('addNestedKeysToRoot', () => { describe('addNestedKeysToRoot', () => {
it('should move the nested keys to root of object', async () => { it('should move the nested keys to root of object', async () => {
const obj = { const obj = {

View File

@@ -2,6 +2,7 @@ const Parse = require('parse/node');
import { isDeepStrictEqual } from 'util'; import { isDeepStrictEqual } from 'util';
import { getRequestObject, resolveError } from './triggers'; import { getRequestObject, resolveError } from './triggers';
import { logger } from './logger'; import { logger } from './logger';
import { LRUCache as LRU } from 'lru-cache';
import RestQuery from './RestQuery'; import RestQuery from './RestQuery';
import RestWrite from './RestWrite'; import RestWrite from './RestWrite';
@@ -67,6 +68,10 @@ function nobody(config) {
return new Auth({ config, isMaster: false }); return new Auth({ config, isMaster: false });
} }
const throttle = new LRU({
max: 10000,
ttl: 500,
});
/** /**
* Checks whether session should be updated based on last update time & session length. * Checks whether session should be updated based on last update time & session length.
*/ */
@@ -78,44 +83,45 @@ function shouldUpdateSessionExpiry(config, session) {
return lastUpdated <= skipRange; return lastUpdated <= skipRange;
} }
const throttle = {};
const renewSessionIfNeeded = async ({ config, session, sessionToken }) => { const renewSessionIfNeeded = async ({ config, session, sessionToken }) => {
if (!config?.extendSessionOnUse) { if (!config?.extendSessionOnUse) {
return; return;
} }
clearTimeout(throttle[sessionToken]); if (throttle.get(sessionToken)) {
throttle[sessionToken] = setTimeout(async () => { return;
try { }
if (!session) { throttle.set(sessionToken, true);
const query = await RestQuery({ try {
method: RestQuery.Method.get, if (!session) {
config, const query = await RestQuery({
auth: master(config), method: RestQuery.Method.get,
runBeforeFind: false,
className: '_Session',
restWhere: { sessionToken },
restOptions: { limit: 1 },
});
const { results } = await query.execute();
session = results[0];
}
if (!shouldUpdateSessionExpiry(config, session) || !session) {
return;
}
const expiresAt = config.generateSessionExpiresAt();
await new RestWrite(
config, config,
master(config), auth: master(config),
'_Session', runBeforeFind: false,
{ objectId: session.objectId }, className: '_Session',
{ expiresAt: Parse._encode(expiresAt) } restWhere: { sessionToken },
).execute(); restOptions: { limit: 1 },
} catch (e) { });
if (e?.code !== Parse.Error.OBJECT_NOT_FOUND) { const { results } = await query.execute();
logger.error('Could not update session expiry: ', e); session = results[0];
}
} }
}, 500);
if (!shouldUpdateSessionExpiry(config, session) || !session) {
return;
}
const expiresAt = config.generateSessionExpiresAt();
await new RestWrite(
config,
master(config),
'_Session',
{ objectId: session.objectId },
{ expiresAt: Parse._encode(expiresAt) }
).execute();
} catch (e) {
if (e?.code !== Parse.Error.OBJECT_NOT_FOUND) {
logger.error('Could not update session expiry: ', e);
}
}
}; };
// Returns a promise that resolves to an Auth object // Returns a promise that resolves to an Auth object

View File

@@ -282,7 +282,6 @@ export class UserController extends AdaptableController {
user = await this.setPasswordResetToken(email); user = await this.setPasswordResetToken(email);
} }
const token = encodeURIComponent(user._perishable_token); const token = encodeURIComponent(user._perishable_token);
const link = buildEmailLink(this.config.requestResetPasswordURL, token, this.config); const link = buildEmailLink(this.config.requestResetPasswordURL, token, this.config);
const options = { const options = {
appName: this.config.appName, appName: this.config.appName,

View File

@@ -399,6 +399,17 @@ class Utils {
} }
return obj; return obj;
} }
/**
* Encodes a string to be used in a URL.
* @param {String} input The string to encode.
* @returns {String} The encoded string.
*/
static encodeForUrl(input) {
return encodeURIComponent(input).replace(/[!'.()*]/g, char =>
'%' + char.charCodeAt(0).toString(16).toUpperCase()
);
}
} }
module.exports = Utils; module.exports = Utils;