diff --git a/discord-bot/.env.example b/discord-bot/.env.example
index 0e069d9..50f57d4 100644
--- a/discord-bot/.env.example
+++ b/discord-bot/.env.example
@@ -5,6 +5,8 @@ DATA_API_URI=http://127.0.0.1:8181/v1/discordbot
DATA_API_AUTH_USERNAME=cassandra
DATA_API_AUTH_PASSWORD=cassandra
DATA_API_AUTH_URL=http://localhost:8081/v1/auth
+#Uncomment the following to enable API tables
+#DATA_API_TABLES=true
#Fill the ASTRA DB related details only when IS_ASTRA is set to 'true'
#IS_ASTRA=true
diff --git a/discord-bot/.env.test b/discord-bot/.env.test
index 7d9ebfe..f958d19 100644
--- a/discord-bot/.env.test
+++ b/discord-bot/.env.test
@@ -1,4 +1,4 @@
-DATA_API_URI=http://127.0.0.1:8181/v1/discordbot_test
+DATA_API_URI=http://127.0.0.1:8181/v1/demo
DATA_API_AUTH_USERNAME=cassandra
DATA_API_AUTH_PASSWORD=cassandra
-DATA_API_AUTH_URL=http://localhost:8081/v1/auth
\ No newline at end of file
+DATA_API_TABLES=true
\ No newline at end of file
diff --git a/discord-bot/commands/countdocuments.js b/discord-bot/commands/countdocuments.js
index 1f798d7..6668e81 100644
--- a/discord-bot/commands/countdocuments.js
+++ b/discord-bot/commands/countdocuments.js
@@ -6,7 +6,7 @@ const Bot = require('../models/bot');
module.exports = {
data: new SlashCommandBuilder().setName('count').setDescription('counts documents in the database'),
async execute(interaction) {
- const num = await Bot.find().then(res => res.length);
+ const num = await Bot.find({}).then(res => res.length);
console.log(new Date(), 'count', num);
await interaction.reply(num.toString());
}
diff --git a/discord-bot/connect.js b/discord-bot/connect.js
index d158284..36984ea 100644
--- a/discord-bot/connect.js
+++ b/discord-bot/connect.js
@@ -17,10 +17,12 @@ module.exports = async function connect() {
};
} else {
uri = process.env.DATA_API_URI;
+ const featureFlags = process.env.DATA_API_TABLES ? ['Feature-Flag-tables'] : [];
jsonApiConnectOptions = {
username: process.env.DATA_API_AUTH_USERNAME,
password: process.env.DATA_API_AUTH_PASSWORD,
- authUrl: process.env.DATA_API_AUTH_URL
+ authUrl: process.env.DATA_API_AUTH_URL,
+ featureFlags
};
}
console.log('Connecting to', uri);
diff --git a/discord-bot/models/bot.js b/discord-bot/models/bot.js
index e49bb77..078d324 100644
--- a/discord-bot/models/bot.js
+++ b/discord-bot/models/bot.js
@@ -4,7 +4,7 @@ const mongoose = require('../mongoose');
const botSchema = new mongoose.Schema({
name: String
-});
+}, { versionKey: false });
const Bot = mongoose.model('Bot', botSchema);
diff --git a/discord-bot/seed.js b/discord-bot/seed.js
index b0c0463..5595ded 100644
--- a/discord-bot/seed.js
+++ b/discord-bot/seed.js
@@ -13,11 +13,30 @@ seed().catch(err => {
async function seed() {
await connect();
-
- const existingCollections = await mongoose.connection.listCollections()
- .then(collections => collections.map(c => c.name));
- if (!existingCollections.includes(Bot.collection.collectionName)) {
- await Bot.createCollection();
+
+ if (process.env.DATA_API_TABLES) {
+ await mongoose.connection.runCommand({
+ createTable: {
+ name: 'bots',
+ definition: {
+ primaryKey: '_id',
+ columns: {
+ _id: {
+ type: 'text'
+ },
+ name: {
+ type: 'text'
+ }
+ }
+ }
+ }
+ });
+ } else {
+ const existingCollections = await mongoose.connection.listCollections()
+ .then(collections => collections.map(c => c.name));
+ if (!existingCollections.includes(Bot.collection.collectionName)) {
+ await Bot.createCollection();
+ }
}
console.log('Done');
diff --git a/discord-bot/test/countdocuments.test.js b/discord-bot/test/countdocuments.test.js
index 50d563a..c35ba94 100644
--- a/discord-bot/test/countdocuments.test.js
+++ b/discord-bot/test/countdocuments.test.js
@@ -8,8 +8,11 @@ const sinon = require('sinon');
describe('countdocuments', function() {
it('returns the number of bot documents', async function() {
- await Bot.deleteMany({});
- await Bot.create({ name: 'test' });
+ const docs = await Bot.find({});
+ for (const doc of docs) {
+ await Bot.deleteOne({ _id: doc._id });
+ }
+ await Bot.insertMany({ name: 'test' });
const interaction = {
reply: sinon.stub()
diff --git a/discord-bot/test/createdocument.test.js b/discord-bot/test/createdocument.test.js
index 0f6d0d4..ffe231e 100644
--- a/discord-bot/test/createdocument.test.js
+++ b/discord-bot/test/createdocument.test.js
@@ -8,7 +8,10 @@ const sinon = require('sinon');
describe('createdocument', function() {
it('inserts a new document', async function() {
- await Bot.deleteMany({});
+ let docs = await Bot.find({});
+ for (const doc of docs) {
+ await Bot.deleteOne({ _id: doc._id });
+ }
const interaction = {
reply: sinon.stub()
@@ -17,7 +20,7 @@ describe('createdocument', function() {
assert.ok(interaction.reply.calledOnce);
assert.deepEqual(interaction.reply.getCalls()[0].args, ['done!']);
- const docs = await Bot.find();
+ docs = await Bot.find({});
assert.equal(docs.length, 1);
assert.equal(docs[0].name, 'I am a document');
});
diff --git a/discord-bot/test/setup.js b/discord-bot/test/setup.js
index 7c8513e..b78a543 100644
--- a/discord-bot/test/setup.js
+++ b/discord-bot/test/setup.js
@@ -7,18 +7,43 @@ const { after, before } = require('mocha');
const mongoose = require('../mongoose');
const uri = process.env.DATA_API_URI;
+const featureFlags = process.env.DATA_API_TABLES ? ['Feature-Flag-tables'] : [];
const jsonApiConnectOptions = {
username: process.env.DATA_API_AUTH_USERNAME,
password: process.env.DATA_API_AUTH_PASSWORD,
- authUrl: process.env.DATA_API_AUTH_URL
+ featureFlags
};
+if (process.env.DATA_API_TABLES) {
+ console.log('Testing Data API tables');
+}
before(async function() {
this.timeout(30000);
await mongoose.connect(uri, jsonApiConnectOptions);
- // dropCollection() can be slower
- await Bot.db.dropCollection('bots').catch(() => {});
- await Bot.createCollection();
+
+ if (process.env.DATA_API_TABLES) {
+ await mongoose.connection.runCommand({
+ createTable: {
+ name: 'bots',
+ definition: {
+ primaryKey: '_id',
+ columns: {
+ _id: {
+ type: 'text'
+ },
+ name: {
+ type: 'text'
+ }
+ }
+ }
+ }
+ });
+ }
+
+ const docs = await Bot.find({});
+ for (const doc of docs) {
+ await Bot.deleteOne({ _id: doc._id });
+ }
});
after(async function() {
diff --git a/netlify-functions-ecommerce/.env.example b/netlify-functions-ecommerce/.env.example
index c896178..e39c989 100644
--- a/netlify-functions-ecommerce/.env.example
+++ b/netlify-functions-ecommerce/.env.example
@@ -3,14 +3,16 @@
# false - if not connecting to an AstraDB and connecting to a local/remote jsonapi instead
IS_ASTRA=
-#Fill the JSON API related details only when IS_ASTRA is set to 'false'
-#Local JSON API URL for example: http://127.0.0.1:8181/v1/ecommerce_test where 'ecommerce_test' is the keyspace name
+#Fill the Data API related details only when IS_ASTRA is set to 'false'
+#Local Data API URL for example: http://127.0.0.1:8181/v1/ecommerce_test where 'ecommerce_test' is the keyspace name
DATA_API_URI=http://127.0.0.1:8181/v1/ecommerce_test
#Auth URL for example: http://127.0.0.1:8081/v1/auth
DATA_API_AUTH_URL=http://127.0.0.1:8081/v1/auth
#Auth username and password
DATA_API_AUTH_USERNAME=cassandra
DATA_API_AUTH_PASSWORD=cassandra
+#Uncomment the following to enable API tables
+#DATA_API_TABLES=true
#Fill the ASTRA DB related details only when IS_ASTRA is set to 'true'
#Astra DB API URL
diff --git a/netlify-functions-ecommerce/.env.test b/netlify-functions-ecommerce/.env.test
index ace48da..a1e6bc4 100644
--- a/netlify-functions-ecommerce/.env.test
+++ b/netlify-functions-ecommerce/.env.test
@@ -1,11 +1,10 @@
#Fill the Local JSON API related details only when NODE_ENV is set to 'jsonapi'
#Local JSON API URL for example: http://127.0.0.1:8181/v1/ecommerce_test where 'ecommerce_test' is the keyspace name
-DATA_API_URI=http://127.0.0.1:8181/v1/ecommerce_test
-#Auth URL for example: http://127.0.0.1:8081/v1/auth
-DATA_API_AUTH_URL=http://127.0.0.1:8081/v1/auth
+DATA_API_URI=http://127.0.0.1:8181/v1/demo
#Auth username and password
DATA_API_AUTH_USERNAME=cassandra
DATA_API_AUTH_PASSWORD=cassandra
+DATA_API_TABLES=true
#Fill in Stripe related details if you want to see Stripe integration.
#Otherwise the sample app will bypass Stripe.
diff --git a/netlify-functions-ecommerce/README.md b/netlify-functions-ecommerce/README.md
index ffc52d4..d9d2b8a 100644
--- a/netlify-functions-ecommerce/README.md
+++ b/netlify-functions-ecommerce/README.md
@@ -12,7 +12,7 @@ Other tools include:
Make sure you have a local stargate instance running as described on the [main page](../README.md) of this repo.
## Running This Example
-### Setting up .env file to run against JSON API
+### Setting up .env file to run against Data API
1. Copy the `.env.example` file to `.env` and fill in the values for the environment variables.
2. Set `IS_ASTRA` to `false`
3. Set `DATA_API_URI` to `http://127.0.0.1:8181/v1/ecommerce_test`
@@ -20,6 +20,7 @@ Make sure you have a local stargate instance running as described on the [main p
5. Set `DATA_API_AUTH_USERNAME` to `cassandra`
6. Set `DATA_API_AUTH_PASSWORD` to `cassandra`
7. Remove `ASTRA_DB_ID`, `ASTRA_DB_REGION`, `ASTRA_DB_KEYSPACE`, `ASTRA_DB_APPLICATION_TOKEN`
+8. (Optional) Set `DATA_API_TABLES=true` to make stargate-mongoose set the `Feature-Flag-tables` headers to enable API tables
### Setting up .env file to run against AstraDB
1. Copy the `.env.example` file to `.env` and fill in the values for the environment variables.
@@ -34,7 +35,7 @@ Make sure you have a local stargate instance running as described on the [main p
1. Run `npm install`
2. Run `npm run seed` to create all collections and insert sample data
3. Run `npm run build` to compile the frontend
-4. (Optional) set `STRIPE_SECRET_KEY` to a test Stripe API key in your `.env` file. This will allow you to enable Stripe checkout.
+4. (Optional) set `STRIPE_SECRET_KEY` to a test Stripe API key in your `.env` file. This will allow you to enable Stripe checkout. Or set `STRIPE_SECRET_KEY` to `test` to skip Stripe scheckout
5. Run `npm start`
Run `npm run test:smoke` to run a smoke test against `http://127.0.0.1:8888` that creates a cart using [Axios](https://masteringjs.io/axios).
6. Visit `http://127.0.0.1:8888/` to see the UI
@@ -70,4 +71,4 @@ Using test
8 passing (112ms)
-```
+```
\ No newline at end of file
diff --git a/netlify-functions-ecommerce/connect.js b/netlify-functions-ecommerce/connect.js
index 0c82b1b..b7dae7c 100644
--- a/netlify-functions-ecommerce/connect.js
+++ b/netlify-functions-ecommerce/connect.js
@@ -23,14 +23,18 @@ module.exports = async function connect() {
process.env.ASTRA_NAMESPACE
);
jsonApiConnectOptions = {
- isAstra: true
+ isAstra: true,
+ level: 'fatal'
};
} else {
uri = process.env.DATA_API_URI;
+ const featureFlags = process.env.DATA_API_TABLES ? ['Feature-Flag-tables'] : [];
jsonApiConnectOptions = {
username: process.env.DATA_API_AUTH_USERNAME,
password: process.env.DATA_API_AUTH_PASSWORD,
- authUrl: process.env.DATA_API_AUTH_URL
+ authUrl: process.env.DATA_API_AUTH_URL,
+ featureFlags,
+ level: 'fatal'
};
}
await mongoose.connect(uri, jsonApiConnectOptions);
diff --git a/netlify-functions-ecommerce/models.js b/netlify-functions-ecommerce/models.js
index 17f590a..0f4adc5 100644
--- a/netlify-functions-ecommerce/models.js
+++ b/netlify-functions-ecommerce/models.js
@@ -7,18 +7,29 @@ const productSchema = new mongoose.Schema({
price: Number,
image: String,
description: String
-});
+}, { versionKey: false });
const Product = mongoose.model('Product', productSchema);
module.exports.Product = Product;
const orderSchema = new mongoose.Schema({
- items: [{
- _id: false,
- productId: { type: mongoose.ObjectId, required: true, ref: 'Product' },
- quantity: { type: Number, required: true, validate: v => v > 0 }
- }],
+ items: {
+ // `items` is stored as an array of JSON strings for compatibility with Data API tables, which do not
+ // support lists of objects currently.
+ type: [{
+ type: String,
+ get(v) {
+ return v == null ? v : JSON.parse(v);
+ },
+ set(v) {
+ if (v == null) {
+ return v;
+ }
+ return typeof v === 'string' ? v : JSON.stringify(v);
+ }
+ }]
+ },
total: {
type: Number,
default: 0
@@ -31,25 +42,45 @@ const orderSchema = new mongoose.Schema({
brand: String,
last4: String
}
-}, { optimisticConcurrency: true });
+}, { versionKey: false });
const Order = mongoose.model('Order', orderSchema);
module.exports.Order = Order;
const cartSchema = new mongoose.Schema({
- items: [{
- _id: false,
- productId: { type: mongoose.ObjectId, required: true, ref: 'Product' },
- quantity: { type: Number, required: true }
- }],
+ items: {
+ // `items` is stored as an array of JSON strings for compatibility with Data API tables, which do not
+ // support lists of objects currently.
+ type: [{
+ type: String,
+ get(v) {
+ return v == null ? v : JSON.parse(v);
+ },
+ set(v) {
+ if (v == null) {
+ return v;
+ }
+ return typeof v === 'string' ? v : JSON.stringify(v);
+ }
+ }]
+ },
orderId: { type: mongoose.ObjectId, ref: 'Order' },
total: Number,
stripeSessionId: { type: String }
-}, { timestamps: true });
+}, { versionKey: false, timestamps: false, toObject: { getters: true }, toJSON: { getters: true } });
cartSchema.virtual('numItems').get(function numItems() {
- return this.items.reduce((sum, item) => sum + item.quantity, 0);
+ if (this.items == null) {
+ return 0;
+ }
+ const items = this.items.map(item => {
+ if (typeof item === 'string') {
+ return JSON.parse(item);
+ }
+ return item;
+ });
+ return items.reduce((sum, item) => sum + item.quantity, 0);
});
const Cart = mongoose.model('Cart', cartSchema);
diff --git a/netlify-functions-ecommerce/netlify/functions/addToCart.js b/netlify-functions-ecommerce/netlify/functions/addToCart.js
index 60ecc06..183493c 100644
--- a/netlify-functions-ecommerce/netlify/functions/addToCart.js
+++ b/netlify-functions-ecommerce/netlify/functions/addToCart.js
@@ -29,13 +29,25 @@ const handler = async(event) => {
};
}
for (const product of event.body.items) {
- const exists = cart.items.find(item => item?.productId?.toString() === product?.productId?.toString());
+ const exists = cart.items?.find(item => item?.productId?.toString() === product?.productId?.toString());
if (!exists && products.find(p => product?.productId?.toString() === p?._id?.toString())) {
- cart.items.push(product);
- await cart.save();
+ cart.items = [
+ ...cart.items,
+ product
+ ];
} else {
- exists.quantity += product.quantity;
- await cart.save();
+ const items = [];
+ for (let i = 0; i < cart.items.length; ++i) {
+ const item = cart.items[i];
+ if (item?.productId?.toString() === product?.productId?.toString()) {
+ continue;
+ }
+ items.push(item);
+ }
+ cart.items = [
+ ...items,
+ { productId: product.productId, quantity: exists.quantity + product.quantity }
+ ];
}
}
@@ -47,10 +59,11 @@ const handler = async(event) => {
return { statusCode: 200, body: JSON.stringify(cart) };
} else {
// If no cartId, create a new cart
- const cart = await Cart.create({ items: event.body.items });
+ const [cart] = await Cart.insertMany([{ items: event.body.items }]);
return { statusCode: 200, body: JSON.stringify(cart) };
}
} catch (error) {
+ console.error(error);
return { statusCode: 500, body: error.toString() };
}
};
diff --git a/netlify-functions-ecommerce/netlify/functions/getCart.js b/netlify-functions-ecommerce/netlify/functions/getCart.js
index 6555623..a88f316 100644
--- a/netlify-functions-ecommerce/netlify/functions/getCart.js
+++ b/netlify-functions-ecommerce/netlify/functions/getCart.js
@@ -14,6 +14,7 @@ const handler = async(event) => {
setOptions({ sanitizeFilter: true });
return { statusCode: 200, body: JSON.stringify({ cart }) };
} catch (error) {
+ console.error(error);
return { statusCode: 500, body: error.toString() };
}
};
diff --git a/netlify-functions-ecommerce/netlify/functions/removeFromCart.js b/netlify-functions-ecommerce/netlify/functions/removeFromCart.js
index e0f5e87..2ccb60e 100644
--- a/netlify-functions-ecommerce/netlify/functions/removeFromCart.js
+++ b/netlify-functions-ecommerce/netlify/functions/removeFromCart.js
@@ -17,15 +17,22 @@ const handler = async(event) => {
return { statusCode: 200, body: cart };
}
if (event.body?.item?.quantity) {
+ cart.items = [
+ ...cart.items.slice(0, index),
+ { ...cart.items[index], quantity: cart.items[index].quantity - event.body.item.quantity },
+ ...cart.items.slice(index + 1)
+ ];
cart.items[index].quantity -= event.body.item.quantity;
if (cart.items[index].quantity <= 0) {
cart.items.splice(index, 1);
}
- await cart.save();
} else {
- cart.items.splice(index, 1);
- await cart.save();
+ cart.items = [
+ ...cart.items.slice(0, index),
+ ...cart.items.slice(index + 1)
+ ];
}
+ await cart.save();
return { statusCode: 200, body: JSON.stringify(cart) };
} catch (error) {
console.log(error);
diff --git a/netlify-functions-ecommerce/public/app.js b/netlify-functions-ecommerce/public/app.js
index 921946f..6572c9b 100644
--- a/netlify-functions-ecommerce/public/app.js
+++ b/netlify-functions-ecommerce/public/app.js
@@ -480,9 +480,6 @@ module.exports = "
\n
All Products
\n
{
/*!*******************************!*\
!*** ./frontend/src/index.js ***!
\*******************************/
@@ -556,7 +553,5 @@ router.replace(window.location.pathname);
app.use(router);
app.mount('#content');
-})();
-
/******/ })()
;
\ No newline at end of file
diff --git a/netlify-functions-ecommerce/seed.js b/netlify-functions-ecommerce/seed.js
index 8b6b48f..e1c9816 100644
--- a/netlify-functions-ecommerce/seed.js
+++ b/netlify-functions-ecommerce/seed.js
@@ -9,47 +9,126 @@ const mongoose = require('./mongoose');
async function createProducts() {
await connect();
- const existingCollections = await mongoose.connection.listCollections()
- .then(collections => collections.map(c => c.name));
- for (const Model of Object.values(models)) {
- if (existingCollections.includes(Model.collection.collectionName)) {
- continue;
+ if (process.env.DATA_API_TABLES) {
+ await mongoose.connection.runCommand({
+ dropTable: {
+ name: 'products'
+ }
+ }).catch(err => {
+ if (err.errors && err.errors.length === 1 && err.errors[0].errorCode === 'CANNOT_DROP_UNKNOWN_TABLE') {
+ return;
+ }
+ throw err;
+ });
+ await mongoose.connection.runCommand({
+ dropTable: {
+ name: 'orders'
+ }
+ }).catch(err => {
+ if (err.errors && err.errors.length === 1 && err.errors[0].errorCode === 'CANNOT_DROP_UNKNOWN_TABLE') {
+ return;
+ }
+ throw err;
+ });
+ await mongoose.connection.runCommand({
+ dropTable: {
+ name: 'carts'
+ }
+ }).catch(err => {
+ if (err.errors && err.errors.length === 1 && err.errors[0].errorCode === 'CANNOT_DROP_UNKNOWN_TABLE') {
+ return;
+ }
+ throw err;
+ });
+
+ await mongoose.connection.runCommand({
+ createTable: {
+ name: 'products',
+ definition: {
+ primaryKey: '_id',
+ columns: {
+ _id: { type: 'text' },
+ name: { type: 'text' },
+ price: { type: 'decimal' },
+ image: { type: 'text' },
+ description: { type: 'text' }
+ }
+ }
+ }
+ });
+ await mongoose.connection.runCommand({
+ createTable: {
+ name: 'orders',
+ definition: {
+ primaryKey: '_id',
+ columns: {
+ _id: { type: 'text' },
+ total: { type: 'decimal' },
+ name: { type: 'text' },
+ paymentMethod: { type: 'map', keyType: 'text', valueType: 'text' },
+ items: { type: 'list', valueType: 'text' }
+ }
+ }
+ }
+ });
+ await mongoose.connection.runCommand({
+ createTable: {
+ name: 'carts',
+ definition: {
+ primaryKey: '_id',
+ columns: {
+ _id: { type: 'text' },
+ items: { type: 'list', valueType: 'text' },
+ orderId: { type: 'text' },
+ total: { type: 'decimal' },
+ stripeSessionId: { type: 'text' }
+ }
+ }
+ }
+ });
+ } else {
+ const existingCollections = await mongoose.connection.listCollections()
+ .then(collections => collections.map(c => c.name));
+ for (const Model of Object.values(models)) {
+ if (existingCollections.includes(Model.collection.collectionName)) {
+ continue;
+ }
+ console.log('Creating', Model.collection.collectionName);
+ await Model.createCollection();
}
- console.log('Creating', Model.collection.collectionName);
- await Model.createCollection();
+ await Promise.all(
+ Object.values(models).map(Model => Model.deleteMany({}))
+ );
}
- await Promise.all(
- Object.values(models).map(Model => Model.deleteMany({}))
- );
+
const { Product } = models;
- await Product.create({
- name: 'iPhone 12',
- price: 499,
- image: '/images/iphone-12.png',
- description: '5G speed. A14 Bionic, the fastest chip in a smartphone. The iPhone 12 features Super Retina XDR display with a 6.1‐inch edge-to-edge OLED display, MagSafe wireless charging, Ceramic Shield with four times better drop performance and Night mode on every camera as well as Ultra Wide and Wide cameras.'
- });
-
- await Product.create({
- name: 'iPhone SE',
- price: 429,
- image: '/images/iphone-se.png',
- description: 'The new iPhone SE delivers best-in-class performance and great photos for an affordable price, if you can live with a small screen'
- });
-
- await Product.create({
- name: 'iPhone 12 Pro',
- price: 799,
- image: '/images/iphone-12-pro.png',
- description: 'Shoot amazing videos and photos with the Ultra Wide, Wide, and Telephoto cameras. Capture your best low-light photos with Night mode. Watch HDR movies and shows on the Super Retina XDR display—the brightest iPhone display yet. Experience unprecedented performance with A13 Bionic for gaming, augmented reality (AR), and photography. And get all-day battery life and a new level of water resistance. All in the first iPhone powerful enough to be called Pro.'
- });
-
- await Product.create({
- name: 'iPhone 11',
- price: 300,
- image: '/images/iphone-11.png',
- description: 'The iPhone 11 offers superb cameras, fast performance and excellent battery life for an affordable price'
- });
+ await Product.insertMany([
+ {
+ name: 'iPhone 12',
+ price: 499,
+ image: '/images/iphone-12.png',
+ description: '5G speed. A14 Bionic, the fastest chip in a smartphone. The iPhone 12 features Super Retina XDR display with a 6.1‐inch edge-to-edge OLED display, MagSafe wireless charging, Ceramic Shield with four times better drop performance and Night mode on every camera as well as Ultra Wide and Wide cameras.'
+ },
+ {
+ name: 'iPhone SE',
+ price: 429,
+ image: '/images/iphone-se.png',
+ description: 'The new iPhone SE delivers best-in-class performance and great photos for an affordable price, if you can live with a small screen'
+ },
+ {
+ name: 'iPhone 12 Pro',
+ price: 799,
+ image: '/images/iphone-12-pro.png',
+ description: 'Shoot amazing videos and photos with the Ultra Wide, Wide, and Telephoto cameras. Capture your best low-light photos with Night mode. Watch HDR movies and shows on the Super Retina XDR display—the brightest iPhone display yet. Experience unprecedented performance with A13 Bionic for gaming, augmented reality (AR), and photography. And get all-day battery life and a new level of water resistance. All in the first iPhone powerful enough to be called Pro.'
+ },
+ {
+ name: 'iPhone 11',
+ price: 300,
+ image: '/images/iphone-11.png',
+ description: 'The iPhone 11 offers superb cameras, fast performance and excellent battery life for an affordable price'
+ }
+ ]);
const products = await Product.find();
console.log('done', products.length, products);
diff --git a/netlify-functions-ecommerce/test/fixtures/createOrder.js b/netlify-functions-ecommerce/test/fixtures/createOrder.js
index 1b17c67..9f564c7 100644
--- a/netlify-functions-ecommerce/test/fixtures/createOrder.js
+++ b/netlify-functions-ecommerce/test/fixtures/createOrder.js
@@ -3,7 +3,6 @@
const { Order } = require('../../models');
module.exports = async function createOrder(params) {
-
const order = await Order.create(params.order);
return { order };
};
diff --git a/netlify-functions-ecommerce/test/setup.test.js b/netlify-functions-ecommerce/test/setup.test.js
index 37fa0aa..3749913 100644
--- a/netlify-functions-ecommerce/test/setup.test.js
+++ b/netlify-functions-ecommerce/test/setup.test.js
@@ -6,12 +6,97 @@ const { after, before } = require('mocha');
const connect = require('../connect');
const mongoose = require('../mongoose');
+if (process.env.DATA_API_TABLES) {
+ console.log('Testing Data API tables');
+}
+
before(async function() {
this.timeout(30000);
await connect();
- await Promise.all(Object.values(mongoose.connection.models).map(Model => Model.createCollection()));
- await Promise.all(Object.values(mongoose.connection.models).map(Model => Model.deleteMany({})));
+ if (process.env.DATA_API_TABLES) {
+ await mongoose.connection.runCommand({
+ dropTable: {
+ name: 'products'
+ }
+ }).catch(err => {
+ if (err.errors && err.errors.length === 1 && err.errors[0].errorCode === 'CANNOT_DROP_UNKNOWN_TABLE') {
+ return;
+ }
+ throw err;
+ });
+ await mongoose.connection.runCommand({
+ dropTable: {
+ name: 'orders'
+ }
+ }).catch(err => {
+ if (err.errors && err.errors.length === 1 && err.errors[0].errorCode === 'CANNOT_DROP_UNKNOWN_TABLE') {
+ return;
+ }
+ throw err;
+ });
+ await mongoose.connection.runCommand({
+ dropTable: {
+ name: 'carts'
+ }
+ }).catch(err => {
+ if (err.errors && err.errors.length === 1 && err.errors[0].errorCode === 'CANNOT_DROP_UNKNOWN_TABLE') {
+ return;
+ }
+ throw err;
+ });
+ await mongoose.connection.runCommand({
+ createTable: {
+ name: 'products',
+ definition: {
+ primaryKey: '_id',
+ columns: {
+ _id: { type: 'text' },
+ name: { type: 'text' },
+ price: { type: 'decimal' },
+ image: { type: 'text' },
+ description: { type: 'text' }
+ }
+ }
+ }
+ });
+ await mongoose.connection.runCommand({
+ createTable: {
+ name: 'orders',
+ definition: {
+ primaryKey: '_id',
+ columns: {
+ _id: { type: 'text' },
+ total: { type: 'decimal' },
+ name: { type: 'text' },
+ paymentMethod: { type: 'map', keyType: 'text', valueType: 'text' },
+ items: { type: 'list', valueType: 'text' }
+ }
+ }
+ }
+ });
+ await mongoose.connection.runCommand({
+ createTable: {
+ name: 'carts',
+ definition: {
+ primaryKey: '_id',
+ columns: {
+ _id: { type: 'text' },
+ items: { type: 'list', valueType: 'text' },
+ orderId: { type: 'text' },
+ total: { type: 'decimal' },
+ stripeSessionId: { type: 'text' }
+ }
+ }
+ }
+ });
+ await Promise.all(Object.values(mongoose.connection.models).map(Model => {
+ return Model.find().then(docs => Promise.all(docs.map(doc => Model.deleteOne({ _id: doc._id }))));
+ }));
+ } else {
+ await Promise.all(Object.values(mongoose.connection.models).map(Model => Model.createCollection()));
+ await Promise.all(Object.values(mongoose.connection.models).map(Model => Model.deleteMany({})));
+ }
});
after(async function() {
diff --git a/typescript-express-reviews/.env.example b/typescript-express-reviews/.env.example
index 0902ac1..a12b32d 100644
--- a/typescript-express-reviews/.env.example
+++ b/typescript-express-reviews/.env.example
@@ -11,6 +11,8 @@ DATA_API_AUTH_URL=http://127.0.0.1:8081/v1/auth
#Auth username and password
DATA_API_AUTH_USERNAME=cassandra
DATA_API_AUTH_PASSWORD=cassandra
+#Uncomment the following to enable API tables
+#DATA_API_TABLES=true
#Fill the ASTRA DB related details only when IS_ASTRA is set to 'true'
#Astra DB API URL
diff --git a/typescript-express-reviews/.env.test b/typescript-express-reviews/.env.test
index 120d479..f958d19 100644
--- a/typescript-express-reviews/.env.test
+++ b/typescript-express-reviews/.env.test
@@ -1,4 +1,4 @@
-DATA_API_URI=http://127.0.0.1:8181/v1/reviews_test
+DATA_API_URI=http://127.0.0.1:8181/v1/demo
DATA_API_AUTH_USERNAME=cassandra
DATA_API_AUTH_PASSWORD=cassandra
-DATA_API_AUTH_URL=http://localhost:8081/v1/auth
\ No newline at end of file
+DATA_API_TABLES=true
\ No newline at end of file
diff --git a/typescript-express-reviews/README.md b/typescript-express-reviews/README.md
index f19b44f..0178440 100644
--- a/typescript-express-reviews/README.md
+++ b/typescript-express-reviews/README.md
@@ -31,3 +31,45 @@ Make sure you have Node.js 14 or higher and a local Stargate instance running as
]
}
```
+
+## With tables
+
+```
+CREATE TABLE authentications (
+ "_id" text,
+ type text,
+ "userId" text,
+ secret text,
+ PRIMARY KEY ("_id"));
+
+CREATE TABLE reviews (
+ "_id" text,
+ rating int,
+ text text,
+ "userId" text,
+ "vehicleId" text,
+ "createdAt" decimal,
+ "updatedAt" decimal,
+ PRIMARY KEY ("_id"));
+
+CREATE TABLE users (
+ "_id" text,
+ email text,
+ "firstName" text,
+ "lastName" text,
+ PRIMARY KEY ("_id"));
+
+CREATE TABLE vehicles (
+ "_id" text,
+ make text,
+ model text,
+ year int,
+ images text,
+ "numReviews" int,
+ "averageReview" decimal,
+ PRIMARY KEY ("_id"));
+
+CREATE INDEX ON reviews ("vehicleId");
+CREATE INDEX ON users (email);
+CREATE INDEX ON authentications ("userId");
+```
\ No newline at end of file
diff --git a/typescript-express-reviews/package.json b/typescript-express-reviews/package.json
index 177f115..8de71e8 100644
--- a/typescript-express-reviews/package.json
+++ b/typescript-express-reviews/package.json
@@ -23,6 +23,7 @@
"stargate-mongoose": "0.6.5"
},
"devDependencies": {
+ "@mongoosejs/studio": "0.0.41",
"@types/bcryptjs": "^2.4.2",
"@types/body-parser": "^1.19.2",
"@types/express": "4.17.21",
diff --git a/typescript-express-reviews/src/api/Review/findByVehicle.ts b/typescript-express-reviews/src/api/Review/findByVehicle.ts
index aabb461..f4fbd41 100644
--- a/typescript-express-reviews/src/api/Review/findByVehicle.ts
+++ b/typescript-express-reviews/src/api/Review/findByVehicle.ts
@@ -1,5 +1,11 @@
import express, { Request, Response } from 'express';
import Review from '../../models/review';
+import User from '../../models/user';
+import Vehicle from '../../models/vehicle';
+
+type ReviewDocument = ReturnType<(typeof Review)['hydrate']>;
+type UserDocument = ReturnType<(typeof User)['hydrate']>;
+type VehicleDocument = ReturnType<(typeof Vehicle)['hydrate']>;
async function findByVehicle (request: Request, response: Response): Promise {
let limit = 5;
@@ -17,13 +23,20 @@ async function findByVehicle (request: Request, response: Response): Promise({ vehicleId: vehicleId }).
sort({ createdAt: -1 }).
skip(skip).
limit(limit).
- populate('user').
- populate('vehicle').
+ //populate('user').
+ //populate('vehicle').
setOptions({ sanitizeFilter: true });
+
+ // TODO: populate doesn't work against tables because lack of $in (see stargate/data-api#1446)
+ for (const review of reviews) {
+ review.user = await User.findOne({ _id: review.userId }).orFail();
+ review.vehicle = await Vehicle.findOne({ _id: review.vehicleId }).orFail();
+ }
+
response.status(200).json({ reviews: reviews });
return;
};
diff --git a/typescript-express-reviews/src/api/User/login.ts b/typescript-express-reviews/src/api/User/login.ts
index 45a6d01..abf218e 100644
--- a/typescript-express-reviews/src/api/User/login.ts
+++ b/typescript-express-reviews/src/api/User/login.ts
@@ -16,10 +16,9 @@ async function login(request: Request, response: Response): Promise {
return;
}
- const authentication = await Authentication.findOne({
- type: 'password',
- userId: user._id
- });
+ const authentication = await Authentication.find({
+ userId: user.id
+ }).then(authentications => authentications.find(auth => auth.type === 'password'));
if (authentication == null) {
response.status(404).json({
error: 'This account does not have a password set'
diff --git a/typescript-express-reviews/src/api/User/register.ts b/typescript-express-reviews/src/api/User/register.ts
index 800912f..50200a4 100644
--- a/typescript-express-reviews/src/api/User/register.ts
+++ b/typescript-express-reviews/src/api/User/register.ts
@@ -23,7 +23,7 @@ async function register(request: Request, response: Response): Promise {
const hash = bcrypt.hashSync(request.body.password, salt);
await Authentication.create({
type: 'password',
- userId: user._id,
+ userId: user.id,
secret: hash
});
response.status(200).json({ user: user });
diff --git a/typescript-express-reviews/src/api/Vehicle/findById.ts b/typescript-express-reviews/src/api/Vehicle/findById.ts
index 0625ee4..a8ca6cb 100644
--- a/typescript-express-reviews/src/api/Vehicle/findById.ts
+++ b/typescript-express-reviews/src/api/Vehicle/findById.ts
@@ -1,6 +1,11 @@
import express, { Request, Response } from 'express';
-import Vehicle from '../../models/vehicle';
import Review from '../../models/review';
+import User from '../../models/user';
+import Vehicle from '../../models/vehicle';
+
+type ReviewDocument = ReturnType<(typeof Review)['hydrate']>;
+type UserDocument = ReturnType<(typeof User)['hydrate']>;
+type VehicleDocument = ReturnType<(typeof Vehicle)['hydrate']>;
async function last5(request: Request, response: Response): Promise {
let limit = 5;
@@ -17,13 +22,19 @@ async function last5(request: Request, response: Response): Promise {
findById({ _id: request.query?._id }).
setOptions({ sanitizeFilter: true });
const reviews = await Review.
- find({ vehicleId }).
+ find({ vehicleId: vehicleId }).
sort({ createdAt: -1 }).
limit(limit).
- populate('user').
- populate('vehicle').
+ //populate('user').
+ //populate('vehicle').
setOptions({ sanitizeFilter: true });
+ // TODO: populate doesn't work against tables because lack of $in (see stargate/data-api#1446)
+ for (const review of reviews) {
+ review.user = await User.findOne({ _id: review.userId }).orFail();
+ review.vehicle = await Vehicle.findOne({ _id: review.vehicleId }).orFail();
+ }
+
response.status(200).json({
vehicle: vehicle,
reviews: reviews
diff --git a/typescript-express-reviews/src/api/index.ts b/typescript-express-reviews/src/api/index.ts
index 395d097..c7e2fe0 100644
--- a/typescript-express-reviews/src/api/index.ts
+++ b/typescript-express-reviews/src/api/index.ts
@@ -11,6 +11,8 @@ import findByVehicle from './Review/findByVehicle';
import bodyParser from 'body-parser';
import connect from '../models/connect';
+import studio from '@mongoosejs/studio';
+
const port = process.env.PORT || 3000;
void async function main() {
@@ -32,6 +34,9 @@ void async function main() {
app.getAsync('/vehicle/find-by-id', findById);
app.getAsync('/review/find-by-vehicle', findByVehicle);
+ app.use('/studio', studio.express('/studio/api'));
+ console.log('Mongoose Studio mounted on /studio');
+
await app.listen(port);
console.log('Listening on port ' + port);
}();
diff --git a/typescript-express-reviews/src/models/authentication.ts b/typescript-express-reviews/src/models/authentication.ts
index ee46210..5dac867 100644
--- a/typescript-express-reviews/src/models/authentication.ts
+++ b/typescript-express-reviews/src/models/authentication.ts
@@ -9,7 +9,7 @@ const schema = new mongoose.Schema({
},
userId: { type: mongoose.Types.ObjectId, required: true },
secret: { type: String, required: true }
-});
+}, { versionKey: false });
const Authentication = mongoose.model('Authentication', schema);
diff --git a/typescript-express-reviews/src/models/connect.ts b/typescript-express-reviews/src/models/connect.ts
index 2243574..bc65a6e 100644
--- a/typescript-express-reviews/src/models/connect.ts
+++ b/typescript-express-reviews/src/models/connect.ts
@@ -22,13 +22,14 @@ export default async function connect() {
console.log('Connecting to', uri);
await mongoose.connect(
uri,
- { isAstra: true } as mongoose.ConnectOptions
+ { isAstra: true, level: 'fatal' } as mongoose.ConnectOptions
);
} else {
console.log('Connecting to', dataAPIURI);
+ const featureFlags = process.env.DATA_API_TABLES ? ['Feature-Flag-tables'] : [];
await mongoose.connect(
dataAPIURI,
- { username, password, authUrl } as mongoose.ConnectOptions
+ { username, password, authUrl, featureFlags } as mongoose.ConnectOptions
);
}
}
diff --git a/typescript-express-reviews/src/models/review.ts b/typescript-express-reviews/src/models/review.ts
index 93f82ad..9692937 100644
--- a/typescript-express-reviews/src/models/review.ts
+++ b/typescript-express-reviews/src/models/review.ts
@@ -18,8 +18,16 @@ const schema = new mongoose.Schema({
vehicleId: {
type: 'ObjectId',
required: true
+ },
+ createdAt: {
+ type: Number,
+ default: () => Date.now()
+ },
+ updatedAt: {
+ type: Number,
+ default: () => Date.now()
}
-}, { timestamps: true });
+}, { versionKey: false, timestamps: true });
schema.virtual('user', {
ref: 'User',
diff --git a/typescript-express-reviews/src/models/user.ts b/typescript-express-reviews/src/models/user.ts
index d3a9b7f..f863a89 100644
--- a/typescript-express-reviews/src/models/user.ts
+++ b/typescript-express-reviews/src/models/user.ts
@@ -14,7 +14,7 @@ const schema = new mongoose.Schema({
type: String,
required: true
}
-});
+}, { versionKey: false });
schema.virtual('displayName').get(function() {
return this.firstName + ' ' + this.lastName.slice(0, 1) + '.';
diff --git a/typescript-express-reviews/src/models/vehicle.ts b/typescript-express-reviews/src/models/vehicle.ts
index a724e88..a384cf7 100644
--- a/typescript-express-reviews/src/models/vehicle.ts
+++ b/typescript-express-reviews/src/models/vehicle.ts
@@ -1,5 +1,7 @@
import mongoose from './mongoose';
+const imagesSchemaType = new mongoose.Schema.Types.Array('images', { type: [String] });
+
const schema = new mongoose.Schema({
make: {
type: String,
@@ -14,9 +16,7 @@ const schema = new mongoose.Schema({
required: true,
validate: (v: number) => Number.isInteger(v) && v >= 1950
},
- images: {
- type: [String]
- },
+ images: [String],
numReviews: {
type: Number,
required: true,
@@ -27,7 +27,7 @@ const schema = new mongoose.Schema({
required: true,
default: 0
}
-});
+}, { versionKey: false });
const Vehicle = mongoose.model('Vehicle', schema);
diff --git a/typescript-express-reviews/src/seed/dropCollections.ts b/typescript-express-reviews/src/seed/dropCollections.ts
index 13e5f70..8b376b9 100644
--- a/typescript-express-reviews/src/seed/dropCollections.ts
+++ b/typescript-express-reviews/src/seed/dropCollections.ts
@@ -12,10 +12,37 @@ dropCollections().catch(err => {
async function dropCollections() {
await connect();
- const collections = await mongoose.connection.listCollections();
- for (const collection of collections) {
- console.log('Dropping', collection.name);
- await mongoose.connection.dropCollection(collection.name);
+ if (process.env.DATA_API_TABLES) {
+ // @ts-ignore
+ await mongoose.connection.runCommand({
+ dropTable: {
+ name: 'authentications'
+ }
+ });
+ // @ts-ignore
+ await mongoose.connection.runCommand({
+ dropTable: {
+ name: 'reviews'
+ }
+ });
+ // @ts-ignore
+ await mongoose.connection.runCommand({
+ dropTable: {
+ name: 'users'
+ }
+ });
+ // @ts-ignore
+ await mongoose.connection.runCommand({
+ dropTable: {
+ name: 'vehicles'
+ }
+ });
+ } else {
+ const collections = await mongoose.connection.listCollections();
+ for (const collection of collections) {
+ console.log('Dropping', collection.name);
+ await mongoose.connection.dropCollection(collection.name);
+ }
}
console.log('Done');
diff --git a/typescript-express-reviews/src/seed/seed.ts b/typescript-express-reviews/src/seed/seed.ts
index 323ae41..3d919a7 100644
--- a/typescript-express-reviews/src/seed/seed.ts
+++ b/typescript-express-reviews/src/seed/seed.ts
@@ -10,6 +10,9 @@ import User from '../models/user';
import Vehicle from '../models/vehicle';
import bcrypt from 'bcryptjs';
+import util from 'util';
+util.inspect.defaultOptions.depth = 6;
+
run().catch(err => {
console.error(err);
process.exit(-1);
@@ -18,23 +21,92 @@ run().catch(err => {
async function run() {
await connect();
- const existingCollections = await mongoose.connection.listCollections()
- .then(collections => collections.map(c => c.name));
+ if (process.env.DATA_API_TABLES) {
+ // @ts-ignore
+ await mongoose.connection.runCommand({
+ createTable: {
+ name: 'authentications',
+ definition: {
+ primaryKey: '_id',
+ columns: {
+ _id: { type: 'text' },
+ type: { type: 'text' },
+ userId: { type: 'text' },
+ secret: { type: 'text' }
+ }
+ }
+ }
+ });
+ // @ts-ignore
+ await mongoose.connection.runCommand({
+ createTable: {
+ name: 'reviews',
+ definition: {
+ primaryKey: '_id',
+ columns: {
+ _id: { type: 'text' },
+ rating: { type: 'int' },
+ text: { type: 'text' },
+ userId: { type: 'text' },
+ vehicleId: { type: 'text' },
+ createdAt: { type: 'decimal' },
+ updatedAt: { type: 'decimal' }
+ }
+ }
+ }
+ });
+ // @ts-ignore
+ await mongoose.connection.runCommand({
+ "createTable": {
+ "name": "users",
+ "definition": {
+ "primaryKey": "_id",
+ "columns": {
+ "_id": { "type": "text" },
+ "email": { "type": "text" },
+ "firstName": { "type": "text" },
+ "lastName": { "type": "text" }
+ }
+ }
+ }
+ });
+ // @ts-ignore
+ await mongoose.connection.runCommand({
+ createTable: {
+ name: 'vehicles',
+ definition: {
+ primaryKey: '_id',
+ columns: {
+ _id: { type: 'text' },
+ make: { type: 'text' },
+ model: { type: 'text' },
+ year: { type: 'int' },
+ images: { type: 'list', valueType: 'text' },
+ numReviews: { type: 'int' },
+ averageReview: { type: 'decimal' }
+ }
+ }
+ }
+ });
+ } else {
+ const existingCollections = await mongoose.connection.listCollections()
+ .then(collections => collections.map(c => c.name));
- for (const Model of Object.values(mongoose.connection.models)) {
-
- // First ensure the collection exists
- if (!existingCollections.includes(Model.collection.collectionName)) {
- console.log('Creating collection', Model.collection.collectionName);
- await mongoose.connection.createCollection(Model.collection.collectionName);
- } else {
- console.log('Resetting collection', Model.collection.collectionName);
+ for (const Model of Object.values(mongoose.connection.models)) {
+
+ // First ensure the collection exists
+ if (!existingCollections.includes(Model.collection.collectionName)) {
+ console.log('Creating collection', Model.collection.collectionName);
+ await mongoose.connection.createCollection(Model.collection.collectionName);
+ } else {
+ console.log('Resetting collection', Model.collection.collectionName);
+ }
+ // Then make sure the collection is empty
+ await Model.deleteMany({});
}
- // Then make sure the collection is empty
- await Model.deleteMany({});
}
- const users = await User.create([
+ const users = await User.insertMany([
{
firstName: 'Dominic',
lastName: 'Toretto',
@@ -47,13 +119,13 @@ async function run() {
}
]);
for (let i = 0; i < users.length; i++) {
- await Authentication.create({
+ await Authentication.insertMany([{
type: 'password',
- userId: users[i]._id,
+ userId: users[i].id,
secret: await bcrypt.hash(users[i].firstName.toLowerCase(), 10)
- });
+ }]);
}
- const vehicles = await Vehicle.create([
+ const vehicles = await Vehicle.insertMany([
{
_id: '0'.repeat(24),
make: 'Tesla',
@@ -80,16 +152,16 @@ async function run() {
}
]);
- await Review.create([
+ await Review.insertMany([
{
- vehicleId: vehicles[1]._id,
- userId: users[0]._id,
+ vehicleId: vehicles[1].id,
+ userId: users[0].id,
text: 'When you live your life a quarter of a mile at a time, it ain\'t just about being fast. I needed a 10 second car, and this car delivers.',
rating: 4
},
{
- vehicleId: vehicles[0]._id,
- userId: users[1]._id,
+ vehicleId: vehicles[0].id,
+ userId: users[1].id,
text: 'I need NOS. My car topped out at 140 miles per hour this morning.',
rating: 3
}
diff --git a/typescript-express-reviews/tests/Review.test.ts b/typescript-express-reviews/tests/Review.test.ts
index 5f8c0c2..0e3c33e 100644
--- a/typescript-express-reviews/tests/Review.test.ts
+++ b/typescript-express-reviews/tests/Review.test.ts
@@ -25,12 +25,12 @@ describe('Review', function() {
};
return res;
};
- const user = await User.create({
+ const [user] = await User.insertMany([{
email: 'test@localhost.com',
firstName: 'Test',
lastName: 'Testerson'
- });
- const vehicle = await Vehicle.create(
+ }]);
+ const [vehicle] = await Vehicle.insertMany([
{
make: 'Tesla',
model: 'Model S',
@@ -42,10 +42,10 @@ describe('Review', function() {
numReviews: 0,
averageReview: 0
}
- );
+ ]);
const req = mockRequest({
- vehicleId: vehicle._id.toString(),
- userId: user._id,
+ vehicleId: vehicle.id.toString(),
+ userId: user.id,
rating: 4,
text: 'The length of this text must be greater than 30 to pass validation.'
});
@@ -65,12 +65,12 @@ describe('Review', function() {
};
return res;
};
- const user = await User.create({
+ const [user] = await User.insertMany([{
email: 'test@localhost.com',
firstName: 'Test',
lastName: 'Testerson'
- });
- const vehicle = await Vehicle.create(
+ }]);
+ const [vehicle] = await Vehicle.insertMany([
{
make: 'Tesla',
model: 'Model S',
@@ -82,19 +82,20 @@ describe('Review', function() {
numReviews: 0,
averageReview: 0
},
- );
+ ]);
for (let i = 0; i < 6; i++) {
await Review.create({
rating: i > 5 ? 5 : i,
text: 'This is a review that must have length greater than 30. ' + i,
- vehicleId: vehicle._id,
- userId: user._id
+ vehicleId: vehicle.id,
+ userId: user.id
});
}
vehicle.numReviews = 6;
vehicle.averageReview = 3;
await vehicle.save();
- const req = mockRequest({ vehicleId: vehicle._id.toString(), limit: 3, skip: 1 });
+ // TODO: skip doesn't work against tables yet
+ const req = mockRequest({ vehicleId: vehicle.id.toString(), limit: 3, skip: 0 });
const res = mockResponse();
await findByVehicle(req, res);
@@ -102,7 +103,7 @@ describe('Review', function() {
assert.equal(reviews.length, 3);
assert.deepEqual(
reviews.map((r: typeof Review) => r.rating),
- [4, 3, 2]
+ [5, 4, 3]
);
// Test that populate worked
diff --git a/typescript-express-reviews/tests/User.test.ts b/typescript-express-reviews/tests/User.test.ts
index a437399..cee0bca 100644
--- a/typescript-express-reviews/tests/User.test.ts
+++ b/typescript-express-reviews/tests/User.test.ts
@@ -34,19 +34,19 @@ describe('User', function() {
});
it('should login a user', async function() {
- const user = await User.create({
+ const [user] = await User.insertMany([{
email: 'test1@localhost.com',
firstName: 'Test',
lastName: 'Testerson'
- });
+ }]);
const salt = bcrypt.genSaltSync(10);
const hash = bcrypt.hashSync('password', salt);
- await Authentication.create({
+ await Authentication.insertMany([{
type: 'password',
- userId: user._id,
+ userId: user.id,
secret: hash
- });
+ }]);
const mockRequest = (body) => ({
body
diff --git a/typescript-express-reviews/tests/Vehicle.test.ts b/typescript-express-reviews/tests/Vehicle.test.ts
index 03c74a1..6b6d6dc 100644
--- a/typescript-express-reviews/tests/Vehicle.test.ts
+++ b/typescript-express-reviews/tests/Vehicle.test.ts
@@ -26,12 +26,12 @@ describe('Vehicle', function() {
};
return res;
};
- const user = await User.create({
+ const [user] = await User.insertMany([{
email: 'test@localhost.com',
firstName: 'Test',
lastName: 'Testerson'
- });
- const vehicle = await Vehicle.create(
+ }]);
+ const [vehicle] = await Vehicle.insertMany([
{
make: 'Tesla',
model: 'Model S',
@@ -43,7 +43,7 @@ describe('Vehicle', function() {
numReviews: 0,
averageReview: 0
}
- );
+ ]);
for (let i = 1; i < 7; i++) {
await Review.create({
rating: i > 5 ? 5 : i,
@@ -55,7 +55,7 @@ describe('Vehicle', function() {
vehicle.numReviews = 5;
vehicle.averageReview = 3;
await vehicle.save();
- const req = mockRequest({ _id: vehicle._id.toString(), limit: 5 });
+ const req = mockRequest({ _id: vehicle.id.toString(), limit: 5 });
const res = mockResponse();
await findById(req, res);
assert(res.json.getCall(0).args[0].vehicle);
diff --git a/typescript-express-reviews/tests/index.test.ts b/typescript-express-reviews/tests/index.test.ts
index 56c51a7..bea0409 100644
--- a/typescript-express-reviews/tests/index.test.ts
+++ b/typescript-express-reviews/tests/index.test.ts
@@ -7,27 +7,156 @@ import { after, before } from 'mocha';
import connect from '../src/models/connect';
import mongoose from 'mongoose';
+import Authentication from '../src/models/authentication';
+import Review from '../src/models/review';
+import User from '../src/models/user';
+import Vehicle from '../src/models/vehicle';
+
+if (process.env.DATA_API_TABLES) {
+ console.log('Testing Data API tables');
+}
+
before(async function() {
this.timeout(30000);
await connect();
- // Make sure all collections are created in Stargate, _after_ calling
- // `connect()`. stargate-mongoose doesn't currently support buffering on
- // connection helpers.
- await Promise.all(Object.values(mongoose.models).map(Model => {
- return Model.createCollection();
- }));
+ if (process.env.DATA_API_TABLES) {
+ // @ts-ignore
+ await mongoose.connection.runCommand({
+ dropTable: {
+ name: 'authentications'
+ }
+ }).catch(err => {
+ if (err.errors && err.errors.length === 1 && err.errors[0].errorCode === 'CANNOT_DROP_UNKNOWN_TABLE') {
+ return;
+ }
+ throw err;
+ });
+ // @ts-ignore
+ await mongoose.connection.runCommand({
+ dropTable: {
+ name: 'reviews'
+ }
+ }).catch(err => {
+ if (err.errors && err.errors.length === 1 && err.errors[0].errorCode === 'CANNOT_DROP_UNKNOWN_TABLE') {
+ return;
+ }
+ throw err;
+ });;
+ // @ts-ignore
+ await mongoose.connection.runCommand({
+ dropTable: {
+ name: 'users'
+ }
+ }).catch(err => {
+ if (err.errors && err.errors.length === 1 && err.errors[0].errorCode === 'CANNOT_DROP_UNKNOWN_TABLE') {
+ return;
+ }
+ throw err;
+ });
+ // @ts-ignore
+ await mongoose.connection.runCommand({
+ dropTable: {
+ name: 'vehicles'
+ }
+ }).catch(err => {
+ if (err.errors && err.errors.length === 1 && err.errors[0].errorCode === 'CANNOT_DROP_UNKNOWN_TABLE') {
+ return;
+ }
+ throw err;
+ });
+
+ // @ts-ignore
+ await mongoose.connection.runCommand({
+ createTable: {
+ name: 'authentications',
+ definition: {
+ primaryKey: '_id',
+ columns: {
+ _id: { type: 'text' },
+ type: { type: 'text' },
+ userId: { type: 'text' },
+ secret: { type: 'text' }
+ }
+ }
+ }
+ });
+ // @ts-ignore
+ await mongoose.connection.runCommand({
+ createTable: {
+ name: 'reviews',
+ definition: {
+ primaryKey: '_id',
+ columns: {
+ _id: { type: 'text' },
+ rating: { type: 'int' },
+ text: { type: 'text' },
+ userId: { type: 'text' },
+ vehicleId: { type: 'text' },
+ createdAt: { type: 'decimal' },
+ updatedAt: { type: 'decimal' }
+ }
+ }
+ }
+ });
+ // @ts-ignore
+ await mongoose.connection.runCommand({
+ createTable: {
+ name: 'users',
+ definition: {
+ primaryKey: '_id',
+ columns: {
+ _id: { type: 'text' },
+ email: { type: 'text' },
+ firstName: { type: 'text' },
+ lastName: { type: 'text' }
+ }
+ }
+ }
+ });
+ // @ts-ignore
+ await mongoose.connection.runCommand({
+ createTable: {
+ name: 'vehicles',
+ definition: {
+ primaryKey: '_id',
+ columns: {
+ _id: { type: 'text' },
+ make: { type: 'text' },
+ model: { type: 'text' },
+ year: { type: 'int' },
+ images: { type: 'list', valueType: 'text' },
+ numReviews: { type: 'int' },
+ averageReview: { type: 'decimal' }
+ }
+ }
+ }
+ });
+ } else {
+ // Make sure all collections are created in Stargate, _after_ calling
+ // `connect()`. stargate-mongoose doesn't currently support buffering on
+ // connection helpers.
+ await Promise.all(Object.values(mongoose.models).map(Model => {
+ return Model.createCollection();
+ }));
+ }
});
// `deleteMany()` currently does nothing
-/*beforeEach(async function clearDb() {
+beforeEach(async function clearDb() {
this.timeout(30000);
- await Promise.all(Object.values(mongoose.models).map(Model => {
- return Model.deleteMany({});
- }));
-});*/
+ if (process.env.DATA_API_TABLES) {
+ await Promise.all(Object.values(mongoose.connection.models).map(Model => {
+ return Model.find().then(docs => Promise.all(docs.map(doc => Model.deleteOne({ _id: doc._id }))));
+ }));
+ } else {
+ await Promise.all(Object.values(mongoose.models).map(Model => {
+ return Model.deleteMany({});
+ }));
+ }
+});
after(async function() {
await mongoose.disconnect();