chore(server): Move api-server in to it's own DIR

This commit is contained in:
Bouncey
2018-08-31 16:04:04 +01:00
committed by mrugesh mohapatra
parent 9fba6bce4c
commit 46a217d0a5
369 changed files with 328 additions and 7431 deletions

View File

@ -0,0 +1,314 @@
/*
* Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
* Microsoft Open Technologies would like to thank its contributors, a list
* of whom are at http://rx.codeplex.com/wikipage?title=Contributors.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you
* may not use this file except in compliance with the License. You may
* obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
* implied. See the License for the specific language governing permissions
* and limitations under the License.
*/
import debugFactory from 'debug';
import { Observable, AnonymousObservable, helpers } from 'rx';
const debug = debugFactory('fcc:ajax$');
const root = typeof window !== 'undefined' ? window : {};
// Gets the proper XMLHttpRequest for support for older IE
function getXMLHttpRequest() {
if (root.XMLHttpRequest) {
return new root.XMLHttpRequest();
} else {
var progId;
try {
var progIds = [
'Msxml2.XMLHTTP',
'Microsoft.XMLHTTP',
'Msxml2.XMLHTTP.4.0'
];
for (var i = 0; i < 3; i++) {
try {
progId = progIds[i];
if (new root.ActiveXObject(progId)) {
break;
}
} catch (e) {
// purposely do nothing
helpers.noop(e);
}
}
return new root.ActiveXObject(progId);
} catch (e) {
throw new Error('XMLHttpRequest is not supported by your browser');
}
}
}
// Get CORS support even for older IE
function getCORSRequest() {
var xhr = new root.XMLHttpRequest();
if ('withCredentials' in xhr) {
return xhr;
} else if (root.XDomainRequest) {
return new XDomainRequest();
} else {
throw new Error('CORS is not supported by your browser');
}
}
function parseXhrResponse(responseType, xhr) {
switch (responseType) {
case 'json':
if ('response' in xhr) {
return xhr.responseType ?
xhr.response :
JSON.parse(xhr.response || xhr.responseText || 'null');
} else {
return JSON.parse(xhr.responseText || 'null');
}
case 'xml':
return xhr.responseXML;
case 'text':
default:
return ('response' in xhr) ? xhr.response : xhr.responseText;
}
}
function normalizeAjaxSuccessEvent(e, xhr, settings) {
return {
response: parseXhrResponse(settings.responseType || xhr.responseType, xhr),
status: xhr.status,
responseType: xhr.responseType,
xhr: xhr,
originalEvent: e
};
}
function normalizeAjaxErrorEvent(e, xhr, type) {
return {
type: type,
status: xhr.status,
xhr: xhr,
originalEvent: e
};
}
/*
* Creates an observable for an Ajax request with either a settings object
* with url, headers, etc or a string for a URL.
*
* @example
* source = Rx.DOM.ajax('/products');
* source = Rx.DOM.ajax( url: 'products', method: 'GET' });
*
* interface Options {
* url: String, // URL of the request
* body?: Object, // The body of the request
* method? = 'GET' : 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
* async? = true: Boolean, // Whether the request is async
* headers?: Object, // optional headers
* crossDomain?: true // if a cross domain request, else false
* }
*
* ajax$(url?: String, options: Options) => Observable[XMLHttpRequest]
*/
export function ajax$(options) {
var settings = {
method: 'GET',
crossDomain: false,
async: true,
headers: {},
responseType: 'text',
createXHR: function() {
return this.crossDomain ? getCORSRequest() : getXMLHttpRequest();
},
normalizeError: normalizeAjaxErrorEvent,
normalizeSuccess: normalizeAjaxSuccessEvent
};
if (typeof options === 'string') {
settings.url = options;
} else {
for (var prop in options) {
if (hasOwnProperty.call(options, prop)) {
settings[prop] = options[prop];
}
}
}
var normalizeError = settings.normalizeError;
var normalizeSuccess = settings.normalizeSuccess;
if (!settings.crossDomain && !settings.headers['X-Requested-With']) {
settings.headers['X-Requested-With'] = 'XMLHttpRequest';
}
settings.hasContent = typeof settings.body !== 'undefined';
return new AnonymousObservable(function(observer) {
var isDone = false;
var xhr;
var processResponse = function(xhr, e) {
var status = xhr.status === 1223 ? 204 : xhr.status;
if ((status >= 200 && status <= 300) || status === 0 || status === '') {
try {
observer.onNext(normalizeSuccess(e, xhr, settings));
observer.onCompleted();
} catch (err) {
observer.onError(err);
}
} else {
observer.onError(normalizeError(e, xhr, 'error'));
}
isDone = true;
};
try {
xhr = settings.createXHR();
} catch (err) {
observer.onError(err);
}
try {
if (settings.user) {
xhr.open(
settings.method,
settings.url,
settings.async,
settings.user,
settings.password
);
} else {
xhr.open(settings.method, settings.url, settings.async);
}
var headers = settings.headers;
for (var header in headers) {
if (hasOwnProperty.call(headers, header)) {
xhr.setRequestHeader(header, headers[header]);
}
}
if (
!xhr.upload ||
(!('withCredentials' in xhr) && root.XDomainRequest)
) {
xhr.onload = function(e) {
if (settings.progressObserver) {
settings.progressObserver.onNext(e);
settings.progressObserver.onCompleted();
}
processResponse(xhr, e);
};
if (settings.progressObserver) {
xhr.onprogress = function(e) {
settings.progressObserver.onNext(e);
};
}
xhr.onerror = function(e) {
if (settings.progressObserver) {
settings.progressObserver.onError(e);
}
observer.onError(normalizeError(e, xhr, 'error'));
isDone = true;
};
xhr.onabort = function(e) {
if (settings.progressObserver) {
settings.progressObserver.onError(e);
}
observer.onError(normalizeError(e, xhr, 'abort'));
isDone = true;
};
} else {
xhr.onreadystatechange = function(e) {
if (xhr.readyState === 4) {
processResponse(xhr, e);
}
};
}
debug(
'ajax$ sending content',
settings.hasContent && settings.body
);
xhr.send(settings.hasContent && settings.body || null);
} catch (err) {
observer.onError(err);
}
return function() {
if (!isDone && xhr.readyState !== 4) { xhr.abort(); }
};
});
}
// Creates an observable sequence from an Ajax POST Request with the body.
// post$(url: String, body: Object) => Observable[Any]
export function post$(url, body) {
try {
body = JSON.stringify(body);
} catch (e) {
return Observable.throw(e);
}
return ajax$({ url, body, method: 'POST' });
}
// postJSON$(url: String, body: Object) => Observable[Object]
export function postJSON$(url, body) {
try {
body = JSON.stringify(body);
} catch (e) {
return Observable.throw(e);
}
return ajax$({
url,
body,
method: 'POST',
responseType: 'json',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json'
},
normalizeError: (e, xhr) => parseXhrResponse('json', xhr)
})
.map(({ response }) => response);
}
// Creates an observable sequence from an Ajax GET Request with the body.
// get$(url: String) => Obserable[Any]
export function get$(url) {
return ajax$({ url: url });
}
/**
* Creates an observable sequence from JSON from an Ajax request
*
* @param {String} url The URL to GET
* @returns {Observable} The observable sequence which contains the parsed JSON
*/
// getJSON$(url: String) => Observable[Object];
export function getJSON$(url) {
return ajax$({
url: url,
responseType: 'json',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json'
},
normalizeError: (e, xhr) => parseXhrResponse('json', xhr)
}).map(({ response }) => response);
}

View File

@ -0,0 +1,9 @@
{
"aboutUrl": "https://www.freecodecamp.org/about",
"defaultProfileImage": "https://s3.amazonaws.com/freecodecamp/camper-image-placeholder.png",
"donateUrl": "https://www.freecodecamp.org/donate",
"forumUrl": "https://forum.freecodecamp.org",
"githubUrl": "https://github.com/freecodecamp/freecodecamp",
"RSA": "https://forum.freecodecamp.org/t/the-read-search-ask-methodology-for-getting-unstuck/137307",
"homeURL": "https://www.freecodecamp.org"
}

View File

@ -0,0 +1,38 @@
import _ from 'lodash/fp';
// we don't store loop protect disable key
export const removeNoprotect = _.replace(/noprotect/gi, '');
export const encodeScriptTags = _.flow(
_.replace(/<script>/gi, 'fccss'),
_.replace(/<\/script>/gi, 'fcces')
);
export const decodeScriptTags = _.flow(
_.replace(/fccss/gi, '<script>'),
_.replace(/fcces/gi, '</script>')
);
export const encodeFormAction = _.replace(
// look for attributes in a form
/<form[^>]*>/,
// val is the string within the opening form tag
// look for an `action` attribute, replace it with a fcc tag
_.replace(/action(\s*?)=/, 'fccfaa$1=')
);
export const decodeFormAction = _.replace(
/<form[^>]*>/,
_.replace(/fccfaa(\s*?)=/, 'action$1=')
);
export const encodeFcc = _.flow(
removeNoprotect,
encodeFormAction,
encodeScriptTags
);
export const decodeFcc = _.flow(
decodeFormAction,
decodeScriptTags
);

View File

@ -0,0 +1,70 @@
import test from 'tape';
import {
encodeScriptTags,
decodeScriptTags,
encodeFormAction,
decodeFormAction,
encodeFcc,
decodeFcc
} from './encode-decode.js';
const scriptDecoded = `
<script>console.log('foo')</script>
`;
const scriptEncoded = `
fccssconsole.log('foo')fcces
`;
test('encodeScriptTags', t => {
t.plan(1);
t.equal(
encodeScriptTags(scriptDecoded),
scriptEncoded
);
});
test('decodeScriptTags', t => {
t.plan(1);
t.equal(
decodeScriptTags(scriptEncoded),
scriptDecoded
);
});
const formDecoded = `
<form action ='path'>foo</form>
`;
const formEncoded = `
<form fccfaa ='path'>foo</form>
`;
test('encodeFormAction', t => {
t.plan(1);
t.equal(
encodeFormAction(formDecoded),
formEncoded
);
});
test('decodeFormAction', t => {
t.plan(1);
t.equal(
decodeFormAction(formEncoded),
formDecoded
);
});
test('encodeFcc', t => {
t.plan(1);
t.equal(
encodeFcc('//noprotect' + scriptDecoded + formDecoded),
'//' + scriptEncoded + formEncoded
);
});
test('decodeFcc', t => {
t.plan(1);
t.equal(
decodeFcc(scriptEncoded + formEncoded),
scriptDecoded + formDecoded
);
});

View File

@ -0,0 +1,10 @@
import _ from 'lodash';
export const alertTypes = _.keyBy([
'success',
'info',
'warning',
'danger'
], _.identity);
export const normalizeAlertType = alertType => alertTypes[alertType] || 'info';

View File

@ -0,0 +1,17 @@
import { pick } from 'lodash';
export function dashify(str) {
return ('' + str)
.toLowerCase()
.replace(/\s/g, '-')
.replace(/[^a-z0-9\-\.]/gi, '')
.replace(/\:/g, '');
}
// todo: unify with server/utils/index.js:dasherize
const dasherize = dashify;
export { dasherize };
export const fixCompletedChallengeItem = obj => pick(
obj,
[ 'id', 'completedDate', 'solution', 'githubLink', 'challengeType', 'files' ]
);

View File

@ -0,0 +1,74 @@
import { AnonymousObservable, Disposable } from 'rx';
const root = typeof window !== 'undefined' ? window : {};
const trash = 'document' in root && root.document.createElement('div');
function destroy(element) {
trash.appendChild(element);
trash.innerHTML = '';
}
export function jsonp$(options) {
let id = 0;
if (typeof options === 'string') {
options = { url: options };
}
return new AnonymousObservable(function(o) {
const settings = {
jsonp: 'JSONPCallback',
async: true,
jsonpCallback: 'rxjsjsonpCallbackscallback_' + (id++).toString(36),
...options
};
let script = root.document.createElement('script');
script.type = 'text/javascript';
script.async = settings.async;
script.src = settings.url.replace(settings.jsonp, settings.jsonpCallback);
root[settings.jsonpCallback] = function(data) {
root[settings.jsonpCallback].called = true;
root[settings.jsonpCallback].data = data;
};
const handler = function(e) {
if (e.type === 'load' && !root[settings.jsonpCallback].called) {
e = { type: 'error' };
}
const status = e.type === 'error' ? 400 : 200;
const data = root[settings.jsonpCallback].data;
if (status === 200) {
o.onNext({
status: status,
responseType: 'jsonp',
response: data,
originalEvent: e
});
o.onCompleted();
} else {
o.onError({
type: 'error',
status: status,
originalEvent: e
});
}
};
script.onload = script.onreadystatechanged = script.onerror = handler;
const head = root.document.getElementsByTagName('head')[0] ||
root.document.documentElement;
head.insertBefore(script, head.firstChild);
return Disposable.create(() => {
script.onload = script.onreadystatechanged = script.onerror = null;
destroy(script);
script = null;
});
});
}

View File

@ -0,0 +1,88 @@
const legacyFrontEndProjects = {
challenges: [
// build-a-personal-portfolio-webpage
'bd7158d8c242eddfaeb5bd13',
// build-a-random-quote-machine
'bd7158d8c442eddfaeb5bd13',
// build-a-pomodoro-clock
'bd7158d8c442eddfaeb5bd0f',
// build-a-javascript-calculator
'bd7158d8c442eddfaeb5bd17',
// show-the-local-weather
'bd7158d8c442eddfaeb5bd10',
// use-the-twitchtv-json-api
'bd7158d8c442eddfaeb5bd1f',
// stylize-stories-on-camper-news
'bd7158d8c442eddfaeb5bd18',
// build-a-wikipedia-viewer
'bd7158d8c442eddfaeb5bd19',
// build-a-tic-tac-toe-game
'bd7158d8c442eedfaeb5bd1c',
// build-a-simon-game
'bd7158d8c442eddfaeb5bd1c'
],
title: 'Legacy Front End Projects',
superBlock: 'legacy-front-end'
};
const legacyBackEndProjects = {
challenges: [
// timestamp microservice
'bd7158d8c443edefaeb5bdef',
// request-header-parser-microservice
'bd7158d8c443edefaeb5bdff',
// url-shortener-microservice
'bd7158d8c443edefaeb5bd0e',
// image-search-abstraction-layer
'bd7158d8c443edefaeb5bdee',
// file-metadata-microservice
'bd7158d8c443edefaeb5bd0f',
// build-a-voting-app
'bd7158d8c443eddfaeb5bdef',
// build-a-nightlife-coordination-app
'bd7158d8c443eddfaeb5bdff',
// chart-the-stock-market
'bd7158d8c443eddfaeb5bd0e',
// manage-a-book-trading-club
'bd7158d8c443eddfaeb5bd0f',
// build-a-pinterest-clone
'bd7158d8c443eddfaeb5bdee'
],
title: 'Legacy Back End Projects',
superBlock: 'legacy-back-end'
};
const legacyDataVisProjects = {
challenges: [
// build-a-markdown-previewer
'bd7157d8c242eddfaeb5bd13',
// build-a-camper-leaderboard
'bd7156d8c242eddfaeb5bd13',
// build-a-recipe-box
'bd7155d8c242eddfaeb5bd13',
// build-the-game-of-life
'bd7154d8c242eddfaeb5bd13',
// build-a-roguelike-dungeon-crawler-game
'bd7153d8c242eddfaeb5bd13',
// visualize-data-with-a-bar-chart
'bd7168d8c242eddfaeb5bd13',
// visualize-data-with-a-scatterplot-graph
'bd7178d8c242eddfaeb5bd13',
// visualize-data-with-a-heat-map
'bd7188d8c242eddfaeb5bd13',
// show-national-contiguity-with-a-force-directed-graph
'bd7198d8c242eddfaeb5bd13',
// map-data-across-the-globe
'bd7108d8c242eddfaeb5bd13'
],
title: 'Legacy Data Visualization Projects',
superBlock: 'legacy-data-visualization'
};
const legacyProjects = [
legacyFrontEndProjects,
legacyBackEndProjects,
legacyDataVisProjects
];
export default legacyProjects;

View File

@ -0,0 +1,74 @@
import emptyProtector from '../app/utils/empty-protector';
export function checkMapData(
{
entities: {
challenge,
block,
superBlock
},
result: { superBlocks }
}
) {
if (
!challenge ||
!block ||
!superBlock ||
!superBlocks ||
!superBlocks.length
) {
throw new Error(
'entities not found, db may not be properly seeded'
);
}
}
// getFirstChallenge(
// map: {
// entities: { challenge: Object, block: Object, superBlock: Object },
// result: [...superBlockDashedName: String]
// }
// ) => Challenge|Void
export function getFirstChallenge({
entities: { superBlock, block, challenge },
result: { superBlocks }
}) {
return challenge[
emptyProtector(block[
emptyProtector(superBlock[
superBlocks[0]
]).blocks[0]
]).challenges[0]
];
}
// let challengeDashedName: String;
// createNameIdMap({
// challenge: {
// [...challengeDashedName ]: Challenge
// }) => {
// challengeIdToName: {
// [ ...challengeId ]: challengeDashedName
// }
// };
export function createNameIdMap({ challenge }) {
return {
challengeIdToName: Object.keys(challenge)
.reduce((map, challengeName) => {
map[challenge[challengeName].id] =
challenge[challengeName].dashedName;
return map;
}, {})
};
}
// addNameIdMap(
// map: { entities; Object, ...rest }
// ) => { ...rest, entities: Object };
export function addNameIdMap({ entities, ...rest }) {
return {
...rest,
entities: {
...entities,
...createNameIdMap(entities)
}
};
}

View File

@ -0,0 +1,225 @@
// originally based off of https://github.com/gulpjs/vinyl
import invariant from 'invariant';
import { Observable } from 'rx';
import castToObservable from '../app/utils/cast-to-observable.js';
// createFileStream(
// files: [...PolyVinyl]
// ) => Observable[...Observable[...PolyVinyl]]
export function createFileStream(files = []) {
return Observable.of(
Observable.from(files)
);
}
// Observable::pipe(
// project(
// file: PolyVinyl
// ) => PolyVinyl|Observable[PolyVinyl]|Promise[PolyVinyl]
// ) => Observable[...Observable[...PolyVinyl]]
export function pipe(project) {
const source = this;
return source.map(
files => files.flatMap(file => castToObservable(project(file)))
);
}
// interface PolyVinyl {
// source: String,
// contents: String,
// name: String,
// ext: String,
// path: String,
// key: String,
// head: String,
// tail: String,
// history: [...String],
// error: Null|Object|Error
// }
// createPoly({
// name: String,
// ext: String,
// contents: String,
// history?: [...String],
// }) => PolyVinyl, throws
export function createPoly({
name,
ext,
contents,
history,
...rest
} = {}) {
invariant(
typeof name === 'string',
'name must be a string but got %s',
name
);
invariant(
typeof ext === 'string',
'ext must be a string, but was %s',
ext
);
invariant(
typeof contents === 'string',
'contents must be a string but got %s',
contents
);
return {
...rest,
history: Array.isArray(history) ? history : [ name + ext ],
name,
ext,
path: name + '.' + ext,
key: name + ext,
contents,
error: null
};
}
// isPoly(poly: Any) => Boolean
export function isPoly(poly) {
return poly &&
typeof poly.contents === 'string' &&
typeof poly.name === 'string' &&
typeof poly.ext === 'string' &&
Array.isArray(poly.history);
}
// checkPoly(poly: Any) => Void, throws
export function checkPoly(poly) {
invariant(
isPoly(poly),
'function should receive a PolyVinyl, but got %s',
poly
);
}
// isEmpty(poly: PolyVinyl) => Boolean, throws
export function isEmpty(poly) {
checkPoly(poly);
return !!poly.contents;
}
// setContent(contents: String, poly: PolyVinyl) => PolyVinyl
// setContent will loose source if set
export function setContent(contents, poly) {
checkPoly(poly);
return {
...poly,
contents,
source: null
};
}
// setExt(ext: String, poly: PolyVinyl) => PolyVinyl
export function setExt(ext, poly) {
checkPoly(poly);
const newPoly = {
...poly,
ext,
path: poly.name + '.' + ext,
key: poly.name + ext
};
newPoly.history = [ ...poly.history, newPoly.path ];
return newPoly;
}
// setName(name: String, poly: PolyVinyl) => PolyVinyl
export function setName(name, poly) {
checkPoly(poly);
const newPoly = {
...poly,
name,
path: name + '.' + poly.ext,
key: name + poly.ext
};
newPoly.history = [ ...poly.history, newPoly.path ];
return newPoly;
}
// setError(error: Object, poly: PolyVinyl) => PolyVinyl
export function setError(error, poly) {
invariant(
typeof error === 'object',
'error must be an object or null, but got %',
error
);
checkPoly(poly);
return {
...poly,
error
};
}
// clearHeadTail(poly: PolyVinyl) => PolyVinyl
export function clearHeadTail(poly) {
checkPoly(poly);
return {
...poly,
head: '',
tail: ''
};
}
// appendToTail (tail: String, poly: PolyVinyl) => PolyVinyl
export function appendToTail(tail, poly) {
checkPoly(poly);
return {
...poly,
tail: poly.tail.concat(tail)
};
}
// compileHeadTail(padding: String, poly: PolyVinyl) => PolyVinyl
export function compileHeadTail(padding = '', poly) {
return clearHeadTail(transformContents(
() => [ poly.head, poly.contents, poly.tail ].join(padding),
poly
));
}
// transformContents(
// wrap: (contents: String) => String,
// poly: PolyVinyl
// ) => PolyVinyl
// transformContents will keep a copy of the original
// code in the `source` property. If the original polyvinyl
// already contains a source, this version will continue as
// the source property
export function transformContents(wrap, poly) {
const newPoly = setContent(
wrap(poly.contents),
poly
);
// if no source exist, set the original contents as source
newPoly.source = poly.source || poly.contents;
return newPoly;
}
// transformHeadTailAndContents(
// wrap: (source: String) => String,
// poly: PolyVinyl
// ) => PolyVinyl
export function transformHeadTailAndContents(wrap, poly) {
return {
...transformContents(
wrap,
poly
),
head: wrap(poly.head),
tail: wrap(poly.tail)
};
}
export function testContents(predicate, poly) {
return !!predicate(poly.contents);
}
export function updateFileFromSpec(spec, poly) {
return setContent(poly.contents, createPoly(spec));
}

View File

@ -0,0 +1,44 @@
import { Observable, Disposable } from 'rx';
import Fetchr from 'fetchr';
function callbackObserver(observer) {
return (err, res) => {
if (err) {
return observer.onError(err);
}
observer.onNext(res);
return observer.onCompleted();
};
}
export default function servicesCreator(options) {
const services = new Fetchr(options);
function readService$({ service: resource, params, config }) {
return Observable.create(observer => {
services.read(
resource,
params,
config,
callbackObserver(observer)
);
return Disposable.create(() => observer.dispose());
});
}
function createService$({ service: resource, params, body, config }) {
return Observable.create(observer => {
services.create(
resource,
params,
body,
config,
callbackObserver(observer)
);
return Disposable.create(() => observer.dispose());
});
}
return {
readService$,
createService$
};
}

View File

@ -0,0 +1,10 @@
export const themes = {
night: 'night',
default: 'default'
};
export const invertTheme = currentTheme => (
!currentTheme || currentTheme === themes.default ?
themes.night :
themes.default
);

View File

@ -0,0 +1,15 @@
import { Observable } from 'rx';
import debug from 'debug';
const log = debug('redux-epic:waitForEpics');
// waitForEpics(epicMiddleware: EpicMiddleware) => Observable[Void]
export default function waitForEpics(epicMiddleware) {
return Observable.defer(() => {
log('calling actions onCompleted');
epicMiddleware.end();
return Observable.merge(epicMiddleware);
})
.last({ defaultValue: null })
.map(() => epicMiddleware.restart());
}