basic entities for attributes (unification of labels and relations)

This commit is contained in:
azivner 2018-08-02 22:48:21 +02:00
parent 040f9185f8
commit 097114c0f2
10 changed files with 253 additions and 25 deletions

View File

@ -630,109 +630,114 @@ imageId</ColNames>
<NotNull>1</NotNull> <NotNull>1</NotNull>
<DefaultExpression>&quot;&quot;</DefaultExpression> <DefaultExpression>&quot;&quot;</DefaultExpression>
</column> </column>
<index id="137" parent="16" name="sqlite_autoindex_relations_1"> <column id="137" parent="16" name="isInheritable">
<Position>10</Position>
<DataType>int|0s</DataType>
<DefaultExpression>0</DefaultExpression>
</column>
<index id="138" parent="16" name="sqlite_autoindex_relations_1">
<NameSurrogate>1</NameSurrogate> <NameSurrogate>1</NameSurrogate>
<ColNames>relationId</ColNames> <ColNames>relationId</ColNames>
<ColumnCollations></ColumnCollations> <ColumnCollations></ColumnCollations>
<Unique>1</Unique> <Unique>1</Unique>
</index> </index>
<index id="138" parent="16" name="IDX_relation_sourceNoteId"> <index id="139" parent="16" name="IDX_relation_sourceNoteId">
<ColNames>sourceNoteId</ColNames> <ColNames>sourceNoteId</ColNames>
<ColumnCollations></ColumnCollations> <ColumnCollations></ColumnCollations>
</index> </index>
<index id="139" parent="16" name="IDX_relation_targetNoteId"> <index id="140" parent="16" name="IDX_relation_targetNoteId">
<ColNames>targetNoteId</ColNames> <ColNames>targetNoteId</ColNames>
<ColumnCollations></ColumnCollations> <ColumnCollations></ColumnCollations>
</index> </index>
<key id="140" parent="16"> <key id="141" parent="16">
<ColNames>relationId</ColNames> <ColNames>relationId</ColNames>
<Primary>1</Primary> <Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_relations_1</UnderlyingIndexName> <UnderlyingIndexName>sqlite_autoindex_relations_1</UnderlyingIndexName>
</key> </key>
<column id="141" parent="17" name="sourceId"> <column id="142" parent="17" name="sourceId">
<Position>1</Position> <Position>1</Position>
<DataType>TEXT|0s</DataType> <DataType>TEXT|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
</column> </column>
<column id="142" parent="17" name="dateCreated"> <column id="143" parent="17" name="dateCreated">
<Position>2</Position> <Position>2</Position>
<DataType>TEXT|0s</DataType> <DataType>TEXT|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
</column> </column>
<index id="143" parent="17" name="sqlite_autoindex_source_ids_1"> <index id="144" parent="17" name="sqlite_autoindex_source_ids_1">
<NameSurrogate>1</NameSurrogate> <NameSurrogate>1</NameSurrogate>
<ColNames>sourceId</ColNames> <ColNames>sourceId</ColNames>
<ColumnCollations></ColumnCollations> <ColumnCollations></ColumnCollations>
<Unique>1</Unique> <Unique>1</Unique>
</index> </index>
<key id="144" parent="17"> <key id="145" parent="17">
<ColNames>sourceId</ColNames> <ColNames>sourceId</ColNames>
<Primary>1</Primary> <Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_source_ids_1</UnderlyingIndexName> <UnderlyingIndexName>sqlite_autoindex_source_ids_1</UnderlyingIndexName>
</key> </key>
<column id="145" parent="18" name="type"> <column id="146" parent="18" name="type">
<Position>1</Position> <Position>1</Position>
<DataType>text|0s</DataType> <DataType>text|0s</DataType>
</column> </column>
<column id="146" parent="18" name="name"> <column id="147" parent="18" name="name">
<Position>2</Position> <Position>2</Position>
<DataType>text|0s</DataType> <DataType>text|0s</DataType>
</column> </column>
<column id="147" parent="18" name="tbl_name"> <column id="148" parent="18" name="tbl_name">
<Position>3</Position> <Position>3</Position>
<DataType>text|0s</DataType> <DataType>text|0s</DataType>
</column> </column>
<column id="148" parent="18" name="rootpage"> <column id="149" parent="18" name="rootpage">
<Position>4</Position> <Position>4</Position>
<DataType>integer|0s</DataType> <DataType>integer|0s</DataType>
</column> </column>
<column id="149" parent="18" name="sql"> <column id="150" parent="18" name="sql">
<Position>5</Position> <Position>5</Position>
<DataType>text|0s</DataType> <DataType>text|0s</DataType>
</column> </column>
<column id="150" parent="19" name="name"> <column id="151" parent="19" name="name">
<Position>1</Position> <Position>1</Position>
</column> </column>
<column id="151" parent="19" name="seq"> <column id="152" parent="19" name="seq">
<Position>2</Position> <Position>2</Position>
</column> </column>
<column id="152" parent="20" name="id"> <column id="153" parent="20" name="id">
<Position>1</Position> <Position>1</Position>
<DataType>INTEGER|0s</DataType> <DataType>INTEGER|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
<SequenceIdentity>1</SequenceIdentity> <SequenceIdentity>1</SequenceIdentity>
</column> </column>
<column id="153" parent="20" name="entityName"> <column id="154" parent="20" name="entityName">
<Position>2</Position> <Position>2</Position>
<DataType>TEXT|0s</DataType> <DataType>TEXT|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
</column> </column>
<column id="154" parent="20" name="entityId"> <column id="155" parent="20" name="entityId">
<Position>3</Position> <Position>3</Position>
<DataType>TEXT|0s</DataType> <DataType>TEXT|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
</column> </column>
<column id="155" parent="20" name="sourceId"> <column id="156" parent="20" name="sourceId">
<Position>4</Position> <Position>4</Position>
<DataType>TEXT|0s</DataType> <DataType>TEXT|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
</column> </column>
<column id="156" parent="20" name="syncDate"> <column id="157" parent="20" name="syncDate">
<Position>5</Position> <Position>5</Position>
<DataType>TEXT|0s</DataType> <DataType>TEXT|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
</column> </column>
<index id="157" parent="20" name="IDX_sync_entityName_entityId"> <index id="158" parent="20" name="IDX_sync_entityName_entityId">
<ColNames>entityName <ColNames>entityName
entityId</ColNames> entityId</ColNames>
<ColumnCollations></ColumnCollations> <ColumnCollations></ColumnCollations>
<Unique>1</Unique> <Unique>1</Unique>
</index> </index>
<index id="158" parent="20" name="IDX_sync_syncDate"> <index id="159" parent="20" name="IDX_sync_syncDate">
<ColNames>syncDate</ColNames> <ColNames>syncDate</ColNames>
<ColumnCollations></ColumnCollations> <ColumnCollations></ColumnCollations>
</index> </index>
<key id="159" parent="20"> <key id="160" parent="20">
<ColNames>id</ColNames> <ColNames>id</ColNames>
<Primary>1</Primary> <Primary>1</Primary>
</key> </key>

View File

@ -0,0 +1,27 @@
create table attributes
(
attributeId TEXT not null primary key,
noteId TEXT not null,
type TEXT not null,
name TEXT not null,
value TEXT default '' not null,
position INT default 0 not null,
dateCreated TEXT not null,
dateModified TEXT not null,
isDeleted INT not null,
hash TEXT default "" not null);
create index IDX_attributes_name_value
on labels (name, value);
create index IDX_attributes_value
on labels (value);
create index IDX_attributes_noteId
on labels (noteId);
INSERT INTO attributes (attributeId, noteId, type, name, value, position, dateCreated, dateModified, isDeleted, hash)
SELECT labelId, noteId, 'label', name, value, position, dateCreated, dateModified, isDeleted, hash FROM labels;
INSERT INTO attributes (attributeId, noteId, type, name, value, position, dateCreated, dateModified, isDeleted, hash)
SELECT relationId, sourceNoteId, 'relation', name, targetNoteId, position, dateCreated, dateModified, isDeleted, hash FROM relations;

41
src/entities/attribute.js Normal file
View File

@ -0,0 +1,41 @@
"use strict";
const Entity = require('./entity');
const repository = require('../services/repository');
const dateUtils = require('../services/date_utils');
const sql = require('../services/sql');
class Attribute extends Entity {
static get tableName() { return "attributes"; }
static get primaryKeyName() { return "attributeId"; }
static get hashedProperties() { return ["attributeId", "noteId", "type", "name", "value", "dateModified", "dateCreated"]; }
async getNote() {
return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]);
}
async beforeSaving() {
super.beforeSaving();
if (!this.value) {
// null value isn't allowed
this.value = "";
}
if (this.position === undefined) {
this.position = 1 + await sql.getValue(`SELECT COALESCE(MAX(position), 0) FROM attributes WHERE noteId = ?`, [this.noteId]);
}
if (!this.isDeleted) {
this.isDeleted = false;
}
if (!this.dateCreated) {
this.dateCreated = dateUtils.nowDate();
}
this.dateModified = dateUtils.nowDate();
}
}
module.exports = Attribute;

View File

@ -3,6 +3,7 @@ const NoteRevision = require('../entities/note_revision');
const Image = require('../entities/image'); const Image = require('../entities/image');
const NoteImage = require('../entities/note_image'); const NoteImage = require('../entities/note_image');
const Branch = require('../entities/branch'); const Branch = require('../entities/branch');
const Attribute = require('../entities/attribute');
const Label = require('../entities/label'); const Label = require('../entities/label');
const Relation = require('../entities/relation'); const Relation = require('../entities/relation');
const RecentNote = require('../entities/recent_note'); const RecentNote = require('../entities/recent_note');
@ -13,7 +14,10 @@ const repository = require('../services/repository');
function createEntityFromRow(row) { function createEntityFromRow(row) {
let entity; let entity;
if (row.labelId) { if (row.attributeId) {
entity = new Attribute(row);
}
else if (row.labelId) {
entity = new Label(row); entity = new Label(row);
} }
else if (row.relationId) { else if (row.relationId) {

View File

@ -0,0 +1,70 @@
"use strict";
const sql = require('../../services/sql');
const attributeService = require('../../services/attributes');
const repository = require('../../services/repository');
const Attribute = require('../../entities/attribute');
async function getNoteAttributes(req) {
const noteId = req.params.noteId;
return await repository.getEntities("SELECT * FROM attributes WHERE isDeleted = 0 AND noteId = ? ORDER BY position, dateCreated", [noteId]);
}
async function updateNoteAttributes(req) {
const noteId = req.params.noteId;
const attributes = req.body;
for (const attribute of attributes) {
let attributeEntity;
if (attribute.attributeId) {
attributeEntity = await repository.getAttribute(attribute.attributeId);
}
else {
// if it was "created" and then immediatelly deleted, we just don't create it at all
if (attribute.isDeleted) {
continue;
}
attributeEntity = new Attribute();
attributeEntity.noteId = noteId;
}
attributeEntity.name = attribute.name;
attributeEntity.value = attribute.value;
attributeEntity.position = attribute.position;
attributeEntity.isDeleted = attribute.isDeleted;
await attributeEntity.save();
}
return await repository.getEntities("SELECT * FROM attributes WHERE isDeleted = 0 AND noteId = ? ORDER BY position, dateCreated", [noteId]);
}
async function getAllAttributeNames() {
const names = await sql.getColumn("SELECT DISTINCT name FROM attributes WHERE isDeleted = 0");
for (const attribute of attributeService.BUILTIN_ATTRIBUTES) {
if (!names.includes(attribute)) {
names.push(attribute);
}
}
names.sort();
return names;
}
async function getValuesForAttribute(req) {
const attributeName = req.params.attributeName;
return await sql.getColumn("SELECT DISTINCT value FROM attributes WHERE isDeleted = 0 AND name = ? AND value != '' ORDER BY value", [attributeName]);
}
module.exports = {
getNoteAttributes,
updateNoteAttributes,
getAllAttributeNames,
getValuesForAttribute
};

View File

@ -25,6 +25,7 @@ const sqlRoute = require('./api/sql');
const anonymizationRoute = require('./api/anonymization'); const anonymizationRoute = require('./api/anonymization');
const cleanupRoute = require('./api/cleanup'); const cleanupRoute = require('./api/cleanup');
const imageRoute = require('./api/image'); const imageRoute = require('./api/image');
const attributesRoute = require('./api/attributes');
const labelsRoute = require('./api/labels'); const labelsRoute = require('./api/labels');
const relationsRoute = require('./api/relations'); const relationsRoute = require('./api/relations');
const scriptRoute = require('./api/script'); const scriptRoute = require('./api/script');
@ -133,6 +134,11 @@ function register(app) {
route(GET, '/api/notes/:noteId/download', [auth.checkApiAuthOrElectron], filesRoute.downloadFile); route(GET, '/api/notes/:noteId/download', [auth.checkApiAuthOrElectron], filesRoute.downloadFile);
apiRoute(GET, '/api/notes/:noteId/attributes', attributesRoute.getNoteAttributes);
apiRoute(PUT, '/api/notes/:noteId/attributes', attributesRoute.updateNoteAttributes);
apiRoute(GET, '/api/attributes/names', attributesRoute.getAllAttributeNames);
apiRoute(GET, '/api/attributes/values/:attributeName', attributesRoute.getValuesForAttribute);
apiRoute(GET, '/api/notes/:noteId/labels', labelsRoute.getNoteLabels); apiRoute(GET, '/api/notes/:noteId/labels', labelsRoute.getNoteLabels);
apiRoute(PUT, '/api/notes/:noteId/labels', labelsRoute.updateNoteLabels); apiRoute(PUT, '/api/notes/:noteId/labels', labelsRoute.updateNoteLabels);
apiRoute(GET, '/api/labels/names', labelsRoute.getAllLabelNames); apiRoute(GET, '/api/labels/names', labelsRoute.getAllLabelNames);

View File

@ -3,7 +3,7 @@
const build = require('./build'); const build = require('./build');
const packageJson = require('../../package'); const packageJson = require('../../package');
const APP_DB_VERSION = 108; const APP_DB_VERSION = 109;
const SYNC_VERSION = 1; const SYNC_VERSION = 1;
module.exports = { module.exports = {

View File

@ -0,0 +1,52 @@
"use strict";
const repository = require('./repository');
const Attribute = require('../entities/attribute');
const BUILTIN_ATTRIBUTES = [
'disableVersioning',
'calendarRoot',
'archived',
'excludeFromExport',
'run',
'manualTransactionHandling',
'disableInclusion',
'appCss',
'hideChildrenOverview'
];
async function getNotesWithAttribute(name, value) {
let notes;
if (value !== undefined) {
notes = await repository.getEntities(`SELECT notes.* FROM notes JOIN attributes USING(noteId)
WHERE notes.isDeleted = 0 AND attributes.isDeleted = 0 AND attributes.name = ? AND attributes.value = ?`, [name, value]);
}
else {
notes = await repository.getEntities(`SELECT notes.* FROM notes JOIN attributes USING(noteId)
WHERE notes.isDeleted = 0 AND attributes.isDeleted = 0 AND attributes.name = ?`, [name]);
}
return notes;
}
async function getNoteWithAttribute(name, value) {
const notes = await getNotesWithAttribute(name, value);
return notes.length > 0 ? notes[0] : null;
}
async function createAttribute(noteId, name, value = "") {
return await new Attribute({
noteId: noteId,
name: name,
value: value
}).save();
}
module.exports = {
getNotesWithAttribute,
getNoteWithAttribute,
createAttribute,
BUILTIN_ATTRIBUTES
};

View File

@ -38,6 +38,10 @@ async function addNoteImageSync(noteImageId, sourceId) {
await addEntitySync("note_images", noteImageId, sourceId); await addEntitySync("note_images", noteImageId, sourceId);
} }
async function addAttributeSync(attributeId, sourceId) {
await addEntitySync("attributes", attributeId, sourceId);
}
async function addLabelSync(labelId, sourceId) { async function addLabelSync(labelId, sourceId) {
await addEntitySync("labels", labelId, sourceId); await addEntitySync("labels", labelId, sourceId);
} }
@ -104,6 +108,7 @@ async function fillAllSyncRows() {
await fillSyncRows("recent_notes", "branchId"); await fillSyncRows("recent_notes", "branchId");
await fillSyncRows("images", "imageId"); await fillSyncRows("images", "imageId");
await fillSyncRows("note_images", "noteImageId"); await fillSyncRows("note_images", "noteImageId");
await fillSyncRows("attributes", "attributeId");
await fillSyncRows("labels", "labelId"); await fillSyncRows("labels", "labelId");
await fillSyncRows("relations", "relationId"); await fillSyncRows("relations", "relationId");
await fillSyncRows("api_tokens", "apiTokenId"); await fillSyncRows("api_tokens", "apiTokenId");
@ -119,6 +124,7 @@ module.exports = {
addRecentNoteSync, addRecentNoteSync,
addImageSync, addImageSync,
addNoteImageSync, addNoteImageSync,
addAttributeSync,
addLabelSync, addLabelSync,
addRelationSync, addRelationSync,
addApiTokenSync, addApiTokenSync,

View File

@ -30,6 +30,9 @@ async function updateEntity(sync, entity, sourceId) {
else if (entityName === 'note_images') { else if (entityName === 'note_images') {
await updateNoteImage(entity, sourceId); await updateNoteImage(entity, sourceId);
} }
else if (entityName === 'attributes') {
await updateAttribute(entity, sourceId);
}
else if (entityName === 'labels') { else if (entityName === 'labels') {
await updateLabel(entity, sourceId); await updateLabel(entity, sourceId);
} }
@ -174,6 +177,20 @@ async function updateNoteImage(entity, sourceId) {
} }
} }
async function updateAttribute(entity, sourceId) {
const origAttribute = await sql.getRow("SELECT * FROM attributes WHERE attributeId = ?", [entity.attributeId]);
if (!origAttribute || origAttribute.dateModified <= entity.dateModified) {
await sql.transactional(async () => {
await sql.replace("attributes", entity);
await syncTableService.addAttributeSync(entity.attributeId, sourceId);
});
log.info("Update/sync attribute " + entity.attributeId);
}
}
async function updateLabel(entity, sourceId) { async function updateLabel(entity, sourceId) {
const origLabel = await sql.getRow("SELECT * FROM labels WHERE labelId = ?", [entity.labelId]); const origLabel = await sql.getRow("SELECT * FROM labels WHERE labelId = ?", [entity.labelId]);