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
############################################################
FROM node:20.18.2-alpine3.20 AS build
FROM node:20.19.0-alpine3.20 AS build
RUN apk --no-cache add \
build-base \
@@ -28,7 +28,7 @@ RUN npm ci --omit=dev --ignore-scripts \
############################################################
# 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

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)

925
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -287,6 +287,7 @@ describe('Cloud Code Logger', () => {
});
xit('should log a changed beforeSave indicating a change', done => {
pending('needs more work.....');
const logController = new LoggerController(new WinstonLoggerAdapter());
Parse.Cloud.beforeSave('MyObject', req => {
@@ -309,7 +310,7 @@ describe('Cloud Code Logger', () => {
done();
})
.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 => {
Parse.Cloud.define('testFunction', () => {

View File

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

View File

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

View File

@@ -69,8 +69,8 @@ const get = function (url, options) {
};
describe('Parse.Query Aggregate testing', () => {
beforeEach(done => {
loadTestData().then(done, done);
beforeEach(async () => {
await loadTestData();
});
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 => {
pending('this test fails. See: https://github.com/parse-community/parse-server/issues/5097');
let emailCalledCount = 0;
const emailAdapter = {
sendVerificationEmail: () => {
@@ -3691,7 +3692,7 @@ describe('Parse.User testing', () => {
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 => {
let objectId;

View File

@@ -89,8 +89,8 @@ describe('public API', () => {
});
describe('public API without publicServerURL', () => {
beforeEach(done => {
reconfigureServer({ appName: 'unused' }).then(done, fail);
beforeEach(async () => {
await reconfigureServer({ appName: 'unused' });
});
it('should get 404 on verify_email', done => {
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', () => {
beforeEach(done => {
reconfigureServer({ appName: 'unused' }).then(done, fail);
beforeEach(async () => {
await reconfigureServer({ appName: 'unused' });
});
it('should get 403 on verify_email', done => {

View File

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

View File

@@ -1224,14 +1224,11 @@ describe('PushController', () => {
},
};
beforeEach(done => {
reconfigureServer({
beforeEach(async () => {
await reconfigureServer({
push: { adapter: pushAdapter },
})
.then(() => {
config = Config.get(Parse.applicationId);
})
.then(done, done.fail);
});
config = Config.get(Parse.applicationId);
});
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', () => {
beforeEach(done => {
beforeEach(async () => {
// Create 1 user with the legacy
createUser().then(done);
await createUser();
});
it('should upgrade legacy session token', done => {

View File

@@ -243,8 +243,8 @@ describe('Personally Identifiable Information', () => {
});
describe('with deprecated configured sensitive fields', () => {
beforeEach(done => {
return reconfigureServer({ userSensitiveFields: ['ssn', 'zip'] }).then(done);
beforeEach(async () => {
await reconfigureServer({ userSensitiveFields: ['ssn', 'zip'] });
});
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', () => {
beforeEach(done => {
reconfigureServer({
beforeEach(async () => {
await reconfigureServer({
protectedFields: {
_User: { '*': ['ssn', 'zip'], 'role:Administrator': [] },
},
}).then(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');
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', () => {
it('should move the nested keys to root of object', async () => {
const obj = {

View File

@@ -2,6 +2,7 @@ const Parse = require('parse/node');
import { isDeepStrictEqual } from 'util';
import { getRequestObject, resolveError } from './triggers';
import { logger } from './logger';
import { LRUCache as LRU } from 'lru-cache';
import RestQuery from './RestQuery';
import RestWrite from './RestWrite';
@@ -67,6 +68,10 @@ function nobody(config) {
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.
*/
@@ -78,44 +83,45 @@ function shouldUpdateSessionExpiry(config, session) {
return lastUpdated <= skipRange;
}
const throttle = {};
const renewSessionIfNeeded = async ({ config, session, sessionToken }) => {
if (!config?.extendSessionOnUse) {
return;
}
clearTimeout(throttle[sessionToken]);
throttle[sessionToken] = setTimeout(async () => {
try {
if (!session) {
const query = await RestQuery({
method: RestQuery.Method.get,
config,
auth: master(config),
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(
if (throttle.get(sessionToken)) {
return;
}
throttle.set(sessionToken, true);
try {
if (!session) {
const query = await RestQuery({
method: RestQuery.Method.get,
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);
}
auth: master(config),
runBeforeFind: false,
className: '_Session',
restWhere: { sessionToken },
restOptions: { limit: 1 },
});
const { results } = await query.execute();
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

View File

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

View File

@@ -399,6 +399,17 @@ class Utils {
}
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;