Merge branch 'main' into audit-logging

This commit is contained in:
sjvans
2022-02-08 13:48:35 +01:00
committed by GitHub
18 changed files with 1934 additions and 2100 deletions

View File

@@ -8,7 +8,7 @@
"mocha": true "mocha": true
}, },
"parserOptions": { "parserOptions": {
"ecmaVersion": 2018 "ecmaVersion": 2020
}, },
"globals": { "globals": {
"SELECT": true, "SELECT": true,

View File

@@ -16,7 +16,7 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: [12.x, 14.x] node-version: [16.x, 14.x, 12.x]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2

View File

@@ -1,2 +0,0 @@
// Incorporate pre-build extensions from...
using from '@capire/common';

24
bookshop/db/init.js Normal file
View File

@@ -0,0 +1,24 @@
/**
* In order to keep basic bookshop sample as simple as possible, we don't add
* reuse dependencies. This db/init.js ensures we still have a minimum set of
* currencies, if not obtained through @capire/common.
*/
module.exports = async (db)=>{
const has_common = db.model.definitions['sap.common.Currencies'].elements.numcode
if (has_common) return
const already_filled = await db.exists('sap.common.Currencies',{code:'EUR'})
if (already_filled) return
await INSERT.into ('sap.common.Currencies') .columns (
'code','symbol','name'
) .rows (
[ 'EUR','€','Euro' ],
[ 'USD','$','US Dollar' ],
[ 'GBP','£','British Pound' ],
[ 'ILS','₪','Shekel' ],
[ 'JPY','¥','Yen' ],
)
}

View File

@@ -3,7 +3,6 @@
"version": "1.0.0", "version": "1.0.0",
"description": "A simple self-contained bookshop service.", "description": "A simple self-contained bookshop service.",
"dependencies": { "dependencies": {
"@capire/common": "*",
"@sap/cds": "^5.0.4", "@sap/cds": "^5.0.4",
"express": "^4.17.1", "express": "^4.17.1",
"passport": "0.4.1" "passport": "0.4.1"

View File

@@ -8,7 +8,9 @@ class CatalogService extends cds.ApplicationService { init(){
this.on ('submitOrder', async req => { this.on ('submitOrder', async req => {
const {book,quantity} = req.data const {book,quantity} = req.data
if (quantity < 1) return req.reject (400,`quantity has to be 1 or more`) if (quantity < 1) return req.reject (400,`quantity has to be 1 or more`)
let {stock} = await SELECT `stock` .from (Books,book) let b = await SELECT `stock` .from (Books,book)
if (!b) return req.error (404,`Book #${book} doesn't exist`)
let {stock} = b
if (quantity > stock) return req.reject (409,`${quantity} exceeds stock for book #${book}`) if (quantity > stock) return req.reject (409,`${quantity} exceeds stock for book #${book}`)
await UPDATE (Books,book) .with ({ stock: stock -= quantity }) await UPDATE (Books,book) .with ({ stock: stock -= quantity })
await this.emit ('OrderedBook', { book, quantity, buyer:req.user.id }) await this.emit ('OrderedBook', { book, quantity, buyer:req.user.id })

View File

@@ -33,3 +33,6 @@ using from '@capire/orders/app/fiori';
// Add data browser // Add data browser
using from '@capire/data-viewer'; using from '@capire/data-viewer';
// Incorporate pre-build extensions from...
using from '@capire/common';

View File

@@ -20,7 +20,7 @@ extend sap.common.Currencies with {
* annotate sap.common.Countries with @cds.persistence.skip:false; * annotate sap.common.Countries with @cds.persistence.skip:false;
*/ */
context sap.common_countries { context sap.common.countries {
extend sap.common.Countries { extend sap.common.Countries {
regions : Composition of many Regions on regions._parent = $self.code; regions : Composition of many Regions on regions._parent = $self.code;

View File

@@ -2,71 +2,118 @@
const GET = (url) => axios.get('/-data'+url) const GET = (url) => axios.get('/-data'+url)
const storageGet = (key, def) => localStorage.getItem('data-viewer:'+key) || def const storageGet = (key, def) => localStorage.getItem('data-viewer:'+key) || def
const storageSet = (key, val) => localStorage.setItem('data-viewer:'+key, val) const storageSet = (key, val) => localStorage.setItem('data-viewer:'+key, val)
const columnKeysFirst = (c1, c2) => {
if (c1.isKey && !c2.isKey) return -1
if (!c1.isKey && c2.isKey) return 1
if (c1.isKey && c2.isKey) return c1.name.localeCompare(c2.name)
return 0 // retain natural order of normal columns
}
const viewer = new Vue ({ const vue = Vue.createApp ({
el:'#app', data() { return {
error: undefined,
dataSource: storageGet('data-source', 'db'),
skip: storageGet('skip', 0),
top: storageGet('top', 20),
entity: storageGet('entity') ? JSON.parse(storageGet('entity')) : undefined,
entities: [],
columns: [],
data: [],
rowDetails: {},
rowKey: storageGet('rowKey')
}},
data: { watch: {
dataSource: storageGet('data-source', 'db'), dataSource: (v) => { storageSet('data-source', v); vue.fetchEntities() },
skip: storageGet('skip', 0), skip: (v) => { storageSet('skip', v); if (vue.entity) vue.fetchData() },
top: storageGet('top', 20), top: (v) => { storageSet('top', v); if (vue.entity) vue.fetchData() },
entity: storageGet('entity') ? JSON.parse(storageGet('entity')) : undefined, },
entities: [],
columns: [], methods: {
data: [],
rowDetails: undefined, async fetchEntities () {
let url = `/Entities`
if (vue.dataSource === 'db') url += `?dataSource=db`
const {data} = await GET(url)
vue.entities = data.value
vue.entities.forEach(entity => entity.columns.sort(columnKeysFirst))
const entity = vue.entity && vue.entities.find(e => e.name === vue.entity.name)
if (entity) { // restore selection from previous fetch
vue.columns = entity.columns
await vue.fetchData(entity)
} else {
vue.entity = undefined
vue.columns = []
vue.data = []
vue.rowDetails = {}
}
}, },
watch: { async inspectEntity (eve) {
dataSource: (v) => { storageSet('data-source', v); viewer.fetchEntities() }, const entity = vue.entity = vue.entities [eve.currentTarget.rowIndex-1]
skip: (v) => { storageSet('skip', v); if (viewer.entity) viewer.fetchData() }, storageSet('entity', JSON.stringify(entity))
top: (v) => { storageSet('top', v); if (viewer.entity) viewer.fetchData() }, vue.columns = vue.entities.find(e => e.name === entity.name).columns
return await this.fetchData()
}, },
methods: { async fetchData () {
let url = `/Data?entity=${vue.entity.name}&$skip=${vue.skip}&$top=${vue.top}`
if (vue.dataSource === 'db') url += `&dataSource=db`
async fetchEntities () { try {
let url = `/Entities` const {data} = await GET(url)
if (viewer.dataSource === 'db') url += `?dataSource=db` // sort data along column order
const {data} = await GET(url) const columnIndexes = {}
viewer.entities = data.value vue.columns.forEach((col, i) => columnIndexes[col.name] = i)
const entity = viewer.entity && viewer.entities.find(e => e.name === viewer.entity.name) vue.data = data.value.map(d => d.record
if (entity) { // restore selection from previous fetch .sort((r1, r2) => columnIndexes[r1.column] - columnIndexes[r2.column])
viewer.columns = entity.columns .map(r => r.data)
await viewer.fetchData(entity) )
} else { const row = vue.data.find(data => vue._makeRowKey(data) === vue.rowKey)
viewer.entity = undefined if (row) vue._setRowDetails(row)
viewer.columns = [] else vue.rowDetails = {}
viewer.data = [] vue.error = undefined
viewer.rowDetails = {} } catch (err) {
} vue.data = []
}, vue.rowDetails = {}
if (err.response?.data?.error) {
vue.error = err.response.data.error
} else {
vue.error = { code:err.code, message:err.message }
}
}
async inspectEntity (eve) { },
const entity = viewer.entity = viewer.entities [eve.currentTarget.rowIndex-1]
storageSet('entity', JSON.stringify(entity))
viewer.columns = viewer.entities.find(e => e.name === entity.name).columns
return await this.fetchData()
},
async fetchData () { inspectRow (eve) {
let url = `/Data?entity=${viewer.entity.name}&$skip=${viewer.skip}&$top=${viewer.top}` vue.rowDetails = {}
if (viewer.dataSource === 'db') url += `&dataSource=db` const selectedRow = eve.currentTarget.rowIndex-1
const {data} = await GET(url) vue.rowKey = vue._makeRowKey(vue.data[selectedRow])
viewer.data = data.value.map(d => d.record.map(r => r.data)) storageSet('rowKey', vue.rowKey)
viewer.rowDetails = undefined vue._setRowDetails(vue.data[selectedRow])
}, },
inspectRow (eve) { _setRowDetails(row) {
viewer.rowDetails = {} vue.rowDetails = {}
const selectedRow = eve.currentTarget.rowIndex-1 row.forEach((line, colIndex) => {
viewer.data[selectedRow].forEach((line, colIndex) => { vue.rowDetails[vue.columns[colIndex].name] = line
viewer.rowDetails[viewer.columns[colIndex].name] = line })
}) },
},
_makeRowKey(row) {
// to identify a row, build a key string out of all key columns' values
return row
.filter((_, colIndex) => vue.columns[colIndex] && vue.columns[colIndex].isKey)
.reduce(((prev, next) => prev += next), '')
},
isActiveRow(row) {
return vue._makeRowKey(row) === vue.rowKey
} }
})
viewer.fetchEntities() }
})
.mount('#app')
vue.fetchEntities()

View File

@@ -2,82 +2,94 @@
<html> <html>
<head> <head>
<title>Data Browser</title> <title>Data Browser</title>
<link rel="stylesheet" href="https://unpkg.com/primitive-ui/dist/css/main.css"> <link rel="stylesheet" href="https://unpkg.com/primitive-ui/dist/css/main.css">
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue"></script> <script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script>
<style> <script src="app.js" defer></script>
.hovering tr:hover td { background: #ebeefc; cursor: pointer; }
.highlight { background: #ebeefc; } <style>
.rating-stars { color:teal } th { position: sticky; top:0; z-index: 2; background-color: white; }
.succeeded { color:teal } .noscroll { overflow: hidden; }
.failed { color:red } .hovering tr:hover td { background: #ebeefc; cursor: pointer; }
.condensed { max-width: 100px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .highlight { background: #ebeefc !important; }
.key { font-weight: bold } .rating-stars { color:teal }
.not-key { font-weight: lighter;} .succeeded { color:teal }
.with-sidebar { display: flex; flex-wrap: wrap; gap: 1rem; } .failed { color:red }
.sidebar { flex-basis: 20rem; flex-grow: 1; } .condensed { max-width: 100px; text-overflow: ellipsis; white-space: nowrap; }
.not-sidebar { flex-basis: 0; flex-grow: 999; min-inline-size: 50%;} .key { font-weight: bold }
.horizontal label { display: inline; } .not-key { font-weight: lighter;}
.horizontal input { width: initial; display: inline; } .with-sidebar { display: flex; flex-wrap: wrap; gap: 1rem; }
</style> .sidebar { flex-basis: 20rem; flex-grow: 1; }
.sidebar-main { height: 100vh; overflow-y: scroll; }
.not-sidebar { flex-basis: 0; flex-grow: 999; min-inline-size: 50%; align-items: stretch;}
.not-sidebar-main { max-height: 40vh; overflow-y: scroll; }
.not-sidebar-sub { max-height: 40vh; overflow-y: scroll; }
.horizontal label { display: inline; }
.horizontal input { width: initial; display: inline; }
.error { color: red; }
</style>
</head> </head>
<body> <body class="noscroll">
<div id='app' class="container"> <div id='app' class="full-container">
<h1> {{ document.title }}{{ entity ? ' &ndash; ' + entity.name : '' }}</h1> <h1>Data Browser &ndash; {{ entity ? entity.name : '' }}</h1>
<div class="with-sidebar"> <div class="with-sidebar">
<div class="sidebar"> <div class="sidebar">
<div class="horizontal" style="padding: 0.75rem 0;"> <div class="horizontal" style="padding: 0.75rem 0;">
<label>Datasource:</label> <label>Datasource:</label>
<input type="radio" id="dataSource-db" value="db" v-model="dataSource"> <input type="radio" id="dataSource-db" value="db" v-model="dataSource">
<label for="dataSource-db">Database</label> <label for="dataSource-db">Database</label>
<input type="radio" id="dataSource-srv" value="service" v-model="dataSource"> <input type="radio" id="dataSource-srv" value="service" v-model="dataSource">
<label for="dataSource-srv">Service</label> <label for="dataSource-srv">Service</label>
</div> </div>
<table id='entities' class="hovering"> <div class="sidebar-main">
<thead> <table id='entities' class="hovering">
<th>Entity Name</th> <thead>
</thead> <th>Entity Name</th>
<tr v-for="e in entities" v-bind:id="e.name" v-on:click="inspectEntity" :class="{'highlight': (entity && e.name === entity.name)}"> </thead>
<td>{{ e.name }}</td> <tr v-for="e in entities" :key="e.name" @click="inspectEntity" :class="{'highlight': (entity && e.name === entity.name)}">
</tr> <td>{{ e.name }}</td>
</table> </tr>
</div> </table>
</div>
<div class="not-sidebar">
<div class="horizontal">
<label for="skip">Skip:</label>
<input id="skip" v-model.lazy="skip" title="No. of entries to skip" type="number" min="0">
<label for="top">Top:</label>
<input id="top" v-model.lazy="top" title="No. of entries to read" type="number" min="0">
</div>
<div v-if="entity">
<table id='data' class="hovering striped-table condensed">
<thead>
<th v-for="col in columns" v-bind:title="col.type" :class="[col.isKey ? 'key' : 'not-key']">{{ col.name }} </th>
</thead>
<tr v-for="(row, index) in data" v-on:click="inspectRow">
<td v-for="d in row" v-bind:title="d">{{ d }}</td>
</tr>
</table>
</div>
<div v-if="rowDetails">
<table id='rowDetails'>
<tr v-for="(key, value) in rowDetails" >
<td class="key">{{ value }}</td>
<td>{{ key }}</td>
</tr>
</table>
</div>
</div>
</div> </div>
<div class="not-sidebar">
<div class="horizontal">
<label for="skip">Skip:</label>
<input id="skip" v-model.lazy="skip" title="No. of entries to skip" type="number" min="0">
<label for="top">Top:</label>
<input id="top" v-model.lazy="top" title="No. of entries to read" type="number" min="0">
</div>
<div v-if="data" class="not-sidebar-main">
<table id='data' class="hovering striped-table condensed">
<thead>
<th v-for="col in columns" :title="col.type" :class="[col.isKey ? 'key' : 'not-key']">{{ col.name }} </th>
</thead>
<tr v-for="row in data" @click="inspectRow" :class="{'highlight': isActiveRow(row)}">
<td v-for="d in row" :title="d">{{ d }}</td>
</tr>
</table>
</div>
<div v-if="error" class="not-sidebar-main error">
Error: {{ error.code ? error.code + ' &ndash; ' + error.message : error.message }}
</div>
<p></p>
<div v-if="rowDetails" class="not-sidebar-sub">
<table id='rowDetails'>
<tr v-for="(key, value) in rowDetails" >
<td class="key">{{ value }}</td>
<td>{{ key }}</td>
</tr>
</table>
</div>
</div>
</div>
</div> </div>
</body> </body>
<script src="app.js"></script>
</html> </html>

View File

@@ -1,7 +1,7 @@
/** /**
* Exposes data + entity metadata * Exposes data + entity metadata
*/ */
//@requires:'admin' @requires:'authenticated-user'
service DataService @( path:'-data' ) { service DataService @( path:'-data' ) {
/** /**

View File

@@ -31,7 +31,7 @@ class DataService extends cds.ApplicationService { init(){
if (!entity) return req.reject(404, 'No such entity: ' + entityName) if (!entity) return req.reject(404, 'No such entity: ' + entityName)
const query = SELECT.from(entity) const query = SELECT.from(entity)
query.SELECT.limit = req.query.SELECT.limit // use $skip / $top from request query.SELECT.limit = req.query.SELECT.limit // forward $skip / $top
const dataSource = findDataSource(dataSourceName, entityName) const dataSource = findDataSource(dataSourceName, entityName)
const res = await dataSource.run(query) const res = await dataSource.run(query)

View File

@@ -64,8 +64,9 @@ annotate AdminService.Books.texts with @(
// Add Value Help for Locales // Add Value Help for Locales
annotate AdminService.Books.texts { annotate AdminService.Books.texts {
locale @ValueList:{entity:'Languages'}; locale @(
locale @Common.ValueListWithFixedValues:true; //show as drop down, not a dialog ValueList.entity:'Languages', Common.ValueListWithFixedValues, //show as drop down, not a dialog
)
} }
// In addition we need to expose Languages through AdminService as a target for ValueList // In addition we need to expose Languages through AdminService as a target for ValueList
using { sap } from '@sap/cds/common'; using { sap } from '@sap/cds/common';

View File

@@ -6,6 +6,9 @@
"start": "cds serve srv/world.cds", "start": "cds serve srv/world.cds",
"start:ts": "cds-ts serve srv/world.cds" "start:ts": "cds-ts serve srv/world.cds"
}, },
"dependencies": {
"@sap/cds": "^5.0.4"
},
"devDependencies": { "devDependencies": {
"@types/jest": "^27.0.2", "@types/jest": "^27.0.2",
"@types/node": "^16.11.6", "@types/node": "^16.11.6",

3665
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -21,10 +21,7 @@
"chai": "^4.3.4", "chai": "^4.3.4",
"chai-as-promised": "^7.1.1", "chai-as-promised": "^7.1.1",
"chai-subset": "^1.6.0", "chai-subset": "^1.6.0",
"sqlite3": "^5" "sqlite3": "npm:@mendix/sqlite3@^5"
},
"overrides": {
"tar": "^4.4.18"
}, },
"scripts": { "scripts": {
"cleanup": "rm -rf node_modules && rm -rf */node_modules && rm -rf */*/node_modules", "cleanup": "rm -rf node_modules && rm -rf */node_modules && rm -rf */*/node_modules",

View File

@@ -1,5 +1,5 @@
subject;rating;reviewer;title;text subject;rating;reviewer;title;text
201;5;adam;Intriguing;Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. 201;5;bob;Intriguing;Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
201;4;bob;Fascinating;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Id diam maecenas ultricies mi eget mauris pharetra et. Risus at ultrices mi tempus imperdiet nulla malesuada pellentesque. Pulvinar mattis nunc sed blandit libero. Facilisis magna etiam tempor orci eu. Nec sagittis aliquam malesuada bibendum arcu. Eu consequat ac felis donec. Ultricies tristique nulla aliquet enim tortor at auctor urna nunc. Tortor posuere ac ut consequat semper viverra nam libero. Amet nisl suscipit adipiscing bibendum est ultricies integer quis auctor. Scelerisque purus semper eget duis at tellus. Elementum tempus egestas sed sed risus pretium. Arcu dictum varius duis at. Amet luctus venenatis lectus magna fringilla urna. Eget velit aliquet sagittis id consectetur purus ut faucibus. Vitae auctor eu augue ut lectus. Fermentum iaculis eu non diam phasellus vestibulum. 201;4;bob;Fascinating;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Id diam maecenas ultricies mi eget mauris pharetra et. Risus at ultrices mi tempus imperdiet nulla malesuada pellentesque. Pulvinar mattis nunc sed blandit libero. Facilisis magna etiam tempor orci eu. Nec sagittis aliquam malesuada bibendum arcu. Eu consequat ac felis donec. Ultricies tristique nulla aliquet enim tortor at auctor urna nunc. Tortor posuere ac ut consequat semper viverra nam libero. Amet nisl suscipit adipiscing bibendum est ultricies integer quis auctor. Scelerisque purus semper eget duis at tellus. Elementum tempus egestas sed sed risus pretium. Arcu dictum varius duis at. Amet luctus venenatis lectus magna fringilla urna. Eget velit aliquet sagittis id consectetur purus ut faucibus. Vitae auctor eu augue ut lectus. Fermentum iaculis eu non diam phasellus vestibulum.
207;2;bob;What is this?;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Libero justo laoreet sit amet cursus sit amet dictum. Nunc faucibus a pellentesque sit. Dis parturient montes nascetur ridiculus mus mauris vitae ultricies. Enim nunc faucibus a pellentesque. Commodo quis imperdiet massa tincidunt nunc pulvinar sapien. Cras ornare arcu dui vivamus. Facilisi etiam dignissim diam quis enim lobortis. Et molestie ac feugiat sed. Urna neque viverra justo nec ultrices dui. Ullamcorper a lacus vestibulum sed arcu non. Volutpat ac tincidunt vitae semper quis. Dignissim sodales ut eu sem. Feugiat in fermentum posuere urna nec. At augue eget arcu dictum varius. 207;2;bob;What is this?;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Libero justo laoreet sit amet cursus sit amet dictum. Nunc faucibus a pellentesque sit. Dis parturient montes nascetur ridiculus mus mauris vitae ultricies. Enim nunc faucibus a pellentesque. Commodo quis imperdiet massa tincidunt nunc pulvinar sapien. Cras ornare arcu dui vivamus. Facilisi etiam dignissim diam quis enim lobortis. Et molestie ac feugiat sed. Urna neque viverra justo nec ultrices dui. Ullamcorper a lacus vestibulum sed arcu non. Volutpat ac tincidunt vitae semper quis. Dignissim sodales ut eu sem. Feugiat in fermentum posuere urna nec. At augue eget arcu dictum varius.
251;3;bob;It's dark...;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Suscipit tellus mauris a diam. Velit aliquet sagittis id consectetur purus ut. Viverra adipiscing at in tellus integer. Vitae elementum curabitur vitae nunc. Mattis ullamcorper velit sed ullamcorper morbi. Diam quis enim lobortis scelerisque. Auctor neque vitae tempus quam pellentesque nec nam aliquam. Semper auctor neque vitae tempus. Quis eleifend quam adipiscing vitae proin. Neque convallis a cras semper auctor neque vitae. Imperdiet massa tincidunt nunc pulvinar sapien et ligula. Sit amet consectetur adipiscing elit ut aliquam purus. Pretium quam vulputate dignissim suspendisse. 251;3;bob;It's dark...;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Suscipit tellus mauris a diam. Velit aliquet sagittis id consectetur purus ut. Viverra adipiscing at in tellus integer. Vitae elementum curabitur vitae nunc. Mattis ullamcorper velit sed ullamcorper morbi. Diam quis enim lobortis scelerisque. Auctor neque vitae tempus quam pellentesque nec nam aliquam. Semper auctor neque vitae tempus. Quis eleifend quam adipiscing vitae proin. Neque convallis a cras semper auctor neque vitae. Imperdiet massa tincidunt nunc pulvinar sapien et ligula. Sit amet consectetur adipiscing elit ut aliquam purus. Pretium quam vulputate dignissim suspendisse.
1 subject rating reviewer title text
2 201 5 adam bob Intriguing Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
3 201 4 bob Fascinating Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Id diam maecenas ultricies mi eget mauris pharetra et. Risus at ultrices mi tempus imperdiet nulla malesuada pellentesque. Pulvinar mattis nunc sed blandit libero. Facilisis magna etiam tempor orci eu. Nec sagittis aliquam malesuada bibendum arcu. Eu consequat ac felis donec. Ultricies tristique nulla aliquet enim tortor at auctor urna nunc. Tortor posuere ac ut consequat semper viverra nam libero. Amet nisl suscipit adipiscing bibendum est ultricies integer quis auctor. Scelerisque purus semper eget duis at tellus. Elementum tempus egestas sed sed risus pretium. Arcu dictum varius duis at. Amet luctus venenatis lectus magna fringilla urna. Eget velit aliquet sagittis id consectetur purus ut faucibus. Vitae auctor eu augue ut lectus. Fermentum iaculis eu non diam phasellus vestibulum.
4 207 2 bob What is this? Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Libero justo laoreet sit amet cursus sit amet dictum. Nunc faucibus a pellentesque sit. Dis parturient montes nascetur ridiculus mus mauris vitae ultricies. Enim nunc faucibus a pellentesque. Commodo quis imperdiet massa tincidunt nunc pulvinar sapien. Cras ornare arcu dui vivamus. Facilisi etiam dignissim diam quis enim lobortis. Et molestie ac feugiat sed. Urna neque viverra justo nec ultrices dui. Ullamcorper a lacus vestibulum sed arcu non. Volutpat ac tincidunt vitae semper quis. Dignissim sodales ut eu sem. Feugiat in fermentum posuere urna nec. At augue eget arcu dictum varius.
5 251 3 bob It's dark... Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Suscipit tellus mauris a diam. Velit aliquet sagittis id consectetur purus ut. Viverra adipiscing at in tellus integer. Vitae elementum curabitur vitae nunc. Mattis ullamcorper velit sed ullamcorper morbi. Diam quis enim lobortis scelerisque. Auctor neque vitae tempus quam pellentesque nec nam aliquam. Semper auctor neque vitae tempus. Quis eleifend quam adipiscing vitae proin. Neque convallis a cras semper auctor neque vitae. Imperdiet massa tincidunt nunc pulvinar sapien et ligula. Sit amet consectetur adipiscing elit ut aliquam purus. Pretium quam vulputate dignissim suspendisse.

View File

@@ -32,7 +32,6 @@ entity Likes {
// Auto-fill reviewers and review dates // Auto-fill reviewers and review dates
annotate Reviews with { annotate Reviews with {
reviewer @cds.on.insert:$user; reviewer @cds.on:{insert:$user};
date @cds.on.insert:$now; date @cds.on:{insert:$now,update:$now};
date @cds.on.update:$now;
} }