build: Release (#9654)
This commit is contained in:
43
.github/workflows/release-prepare-monthly.yml
vendored
Normal file
43
.github/workflows/release-prepare-monthly.yml
vendored
Normal 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`.
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
925
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
20
package.json
20
package.json
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
70
src/Auth.js
70
src/Auth.js
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
11
src/Utils.js
11
src/Utils.js
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user