mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 18:36:30 +01:00 
			
		
		
		
	basic entities for attributes (unification of labels and relations)
This commit is contained in:
		| @@ -630,109 +630,114 @@ imageId</ColNames> | ||||
|       <NotNull>1</NotNull> | ||||
|       <DefaultExpression>""</DefaultExpression> | ||||
|     </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> | ||||
|       <ColNames>relationId</ColNames> | ||||
|       <ColumnCollations></ColumnCollations> | ||||
|       <Unique>1</Unique> | ||||
|     </index> | ||||
|     <index id="138" parent="16" name="IDX_relation_sourceNoteId"> | ||||
|     <index id="139" parent="16" name="IDX_relation_sourceNoteId"> | ||||
|       <ColNames>sourceNoteId</ColNames> | ||||
|       <ColumnCollations></ColumnCollations> | ||||
|     </index> | ||||
|     <index id="139" parent="16" name="IDX_relation_targetNoteId"> | ||||
|     <index id="140" parent="16" name="IDX_relation_targetNoteId"> | ||||
|       <ColNames>targetNoteId</ColNames> | ||||
|       <ColumnCollations></ColumnCollations> | ||||
|     </index> | ||||
|     <key id="140" parent="16"> | ||||
|     <key id="141" parent="16"> | ||||
|       <ColNames>relationId</ColNames> | ||||
|       <Primary>1</Primary> | ||||
|       <UnderlyingIndexName>sqlite_autoindex_relations_1</UnderlyingIndexName> | ||||
|     </key> | ||||
|     <column id="141" parent="17" name="sourceId"> | ||||
|     <column id="142" parent="17" name="sourceId"> | ||||
|       <Position>1</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <column id="142" parent="17" name="dateCreated"> | ||||
|     <column id="143" parent="17" name="dateCreated"> | ||||
|       <Position>2</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </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> | ||||
|       <ColNames>sourceId</ColNames> | ||||
|       <ColumnCollations></ColumnCollations> | ||||
|       <Unique>1</Unique> | ||||
|     </index> | ||||
|     <key id="144" parent="17"> | ||||
|     <key id="145" parent="17"> | ||||
|       <ColNames>sourceId</ColNames> | ||||
|       <Primary>1</Primary> | ||||
|       <UnderlyingIndexName>sqlite_autoindex_source_ids_1</UnderlyingIndexName> | ||||
|     </key> | ||||
|     <column id="145" parent="18" name="type"> | ||||
|     <column id="146" parent="18" name="type"> | ||||
|       <Position>1</Position> | ||||
|       <DataType>text|0s</DataType> | ||||
|     </column> | ||||
|     <column id="146" parent="18" name="name"> | ||||
|     <column id="147" parent="18" name="name"> | ||||
|       <Position>2</Position> | ||||
|       <DataType>text|0s</DataType> | ||||
|     </column> | ||||
|     <column id="147" parent="18" name="tbl_name"> | ||||
|     <column id="148" parent="18" name="tbl_name"> | ||||
|       <Position>3</Position> | ||||
|       <DataType>text|0s</DataType> | ||||
|     </column> | ||||
|     <column id="148" parent="18" name="rootpage"> | ||||
|     <column id="149" parent="18" name="rootpage"> | ||||
|       <Position>4</Position> | ||||
|       <DataType>integer|0s</DataType> | ||||
|     </column> | ||||
|     <column id="149" parent="18" name="sql"> | ||||
|     <column id="150" parent="18" name="sql"> | ||||
|       <Position>5</Position> | ||||
|       <DataType>text|0s</DataType> | ||||
|     </column> | ||||
|     <column id="150" parent="19" name="name"> | ||||
|     <column id="151" parent="19" name="name"> | ||||
|       <Position>1</Position> | ||||
|     </column> | ||||
|     <column id="151" parent="19" name="seq"> | ||||
|     <column id="152" parent="19" name="seq"> | ||||
|       <Position>2</Position> | ||||
|     </column> | ||||
|     <column id="152" parent="20" name="id"> | ||||
|     <column id="153" parent="20" name="id"> | ||||
|       <Position>1</Position> | ||||
|       <DataType>INTEGER|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|       <SequenceIdentity>1</SequenceIdentity> | ||||
|     </column> | ||||
|     <column id="153" parent="20" name="entityName"> | ||||
|     <column id="154" parent="20" name="entityName"> | ||||
|       <Position>2</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <column id="154" parent="20" name="entityId"> | ||||
|     <column id="155" parent="20" name="entityId"> | ||||
|       <Position>3</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <column id="155" parent="20" name="sourceId"> | ||||
|     <column id="156" parent="20" name="sourceId"> | ||||
|       <Position>4</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <column id="156" parent="20" name="syncDate"> | ||||
|     <column id="157" parent="20" name="syncDate"> | ||||
|       <Position>5</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <index id="157" parent="20" name="IDX_sync_entityName_entityId"> | ||||
|     <index id="158" parent="20" name="IDX_sync_entityName_entityId"> | ||||
|       <ColNames>entityName | ||||
| entityId</ColNames> | ||||
|       <ColumnCollations></ColumnCollations> | ||||
|       <Unique>1</Unique> | ||||
|     </index> | ||||
|     <index id="158" parent="20" name="IDX_sync_syncDate"> | ||||
|     <index id="159" parent="20" name="IDX_sync_syncDate"> | ||||
|       <ColNames>syncDate</ColNames> | ||||
|       <ColumnCollations></ColumnCollations> | ||||
|     </index> | ||||
|     <key id="159" parent="20"> | ||||
|     <key id="160" parent="20"> | ||||
|       <ColNames>id</ColNames> | ||||
|       <Primary>1</Primary> | ||||
|     </key> | ||||
|   | ||||
							
								
								
									
										27
									
								
								db/migrations/0109__create_attributes.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								db/migrations/0109__create_attributes.sql
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										41
									
								
								src/entities/attribute.js
									
									
									
									
									
										Normal 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; | ||||
| @@ -3,6 +3,7 @@ const NoteRevision = require('../entities/note_revision'); | ||||
| const Image = require('../entities/image'); | ||||
| const NoteImage = require('../entities/note_image'); | ||||
| const Branch = require('../entities/branch'); | ||||
| const Attribute = require('../entities/attribute'); | ||||
| const Label = require('../entities/label'); | ||||
| const Relation = require('../entities/relation'); | ||||
| const RecentNote = require('../entities/recent_note'); | ||||
| @@ -13,7 +14,10 @@ const repository = require('../services/repository'); | ||||
| function createEntityFromRow(row) { | ||||
|     let entity; | ||||
|  | ||||
|     if (row.labelId) { | ||||
|     if (row.attributeId) { | ||||
|         entity = new Attribute(row); | ||||
|     } | ||||
|     else if (row.labelId) { | ||||
|         entity = new Label(row); | ||||
|     } | ||||
|     else if (row.relationId) { | ||||
|   | ||||
							
								
								
									
										70
									
								
								src/routes/api/attributes.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								src/routes/api/attributes.js
									
									
									
									
									
										Normal 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 | ||||
| }; | ||||
| @@ -25,6 +25,7 @@ const sqlRoute = require('./api/sql'); | ||||
| const anonymizationRoute = require('./api/anonymization'); | ||||
| const cleanupRoute = require('./api/cleanup'); | ||||
| const imageRoute = require('./api/image'); | ||||
| const attributesRoute = require('./api/attributes'); | ||||
| const labelsRoute = require('./api/labels'); | ||||
| const relationsRoute = require('./api/relations'); | ||||
| const scriptRoute = require('./api/script'); | ||||
| @@ -133,6 +134,11 @@ function register(app) { | ||||
|  | ||||
|     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(PUT, '/api/notes/:noteId/labels', labelsRoute.updateNoteLabels); | ||||
|     apiRoute(GET, '/api/labels/names', labelsRoute.getAllLabelNames); | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
| const build = require('./build'); | ||||
| const packageJson = require('../../package'); | ||||
|  | ||||
| const APP_DB_VERSION = 108; | ||||
| const APP_DB_VERSION = 109; | ||||
| const SYNC_VERSION = 1; | ||||
|  | ||||
| module.exports = { | ||||
|   | ||||
							
								
								
									
										52
									
								
								src/services/attributes.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/services/attributes.js
									
									
									
									
									
										Normal 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 | ||||
| }; | ||||
| @@ -38,6 +38,10 @@ async function addNoteImageSync(noteImageId, sourceId) { | ||||
|     await addEntitySync("note_images", noteImageId, sourceId); | ||||
| } | ||||
|  | ||||
| async function addAttributeSync(attributeId, sourceId) { | ||||
|     await addEntitySync("attributes", attributeId, sourceId); | ||||
| } | ||||
|  | ||||
| async function addLabelSync(labelId, sourceId) { | ||||
|     await addEntitySync("labels", labelId, sourceId); | ||||
| } | ||||
| @@ -104,6 +108,7 @@ async function fillAllSyncRows() { | ||||
|     await fillSyncRows("recent_notes", "branchId"); | ||||
|     await fillSyncRows("images", "imageId"); | ||||
|     await fillSyncRows("note_images", "noteImageId"); | ||||
|     await fillSyncRows("attributes", "attributeId"); | ||||
|     await fillSyncRows("labels", "labelId"); | ||||
|     await fillSyncRows("relations", "relationId"); | ||||
|     await fillSyncRows("api_tokens", "apiTokenId"); | ||||
| @@ -119,6 +124,7 @@ module.exports = { | ||||
|     addRecentNoteSync, | ||||
|     addImageSync, | ||||
|     addNoteImageSync, | ||||
|     addAttributeSync, | ||||
|     addLabelSync, | ||||
|     addRelationSync, | ||||
|     addApiTokenSync, | ||||
|   | ||||
| @@ -30,6 +30,9 @@ async function updateEntity(sync, entity, sourceId) { | ||||
|     else if (entityName === 'note_images') { | ||||
|         await updateNoteImage(entity, sourceId); | ||||
|     } | ||||
|     else if (entityName === 'attributes') { | ||||
|         await updateAttribute(entity, sourceId); | ||||
|     } | ||||
|     else if (entityName === 'labels') { | ||||
|         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) { | ||||
|     const origLabel = await sql.getRow("SELECT * FROM labels WHERE labelId = ?", [entity.labelId]); | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user