Răsfoiți Sursa

chore: update version to 0.2.11 and add developer notes for Logseq DB properties

charlie 2 săptămâni în urmă
părinte
comite
a2bfb39632

+ 293 - 0
libs/development-notes/db_properties_notes.md

@@ -0,0 +1,293 @@
+# Logseq DB Properties (Developer Notes)
+
+This note explains how **properties** work in Logseq when using a **DB graph** (database mode), how that differs from a **file graph** (Markdown/Org), and how to use the **plugin SDK** APIs defined in `libs/src/LSPlugin.ts`.
+
+> Scope: This is written for plugin developers. It focuses on `logseq.Editor.*` property APIs and tag/class modeling.
+
+---
+
+## 1) Two worlds: File graph vs DB graph
+
+### File graph (Markdown / Org)
+- “Properties” are primarily **text syntax** stored in the file:
+  - Markdown frontmatter (`---`)
+  - Org property drawers (`:PROPERTIES:`)
+  - Inline props (`key:: value`)
+- When you update a property, you’re often ultimately **editing text**.
+- SDK calls like `Editor.insertBlock`/`updateBlock` have `opts.properties`, but note this warning in the typings:
+  - `IBatchBlock.properties` is **not supported for DB graph**.
+
+### DB graph (Database mode)
+DB mode treats properties as **first-class DB entities**.
+
+Think of it as a 2-layer model:
+
+1) **Property definitions (schema)**
+   - A property key (e.g. `rating`, `authors`, `zotero_key`) exists as an entity.
+   - It may have a schema: type, cardinality, visibility.
+
+2) **Property values (data)**
+   - Blocks/pages can store values for property keys.
+   - Persisted structurally in the DB (not by emitting `key:: value` text).
+
+In DB graphs, prefer the dedicated APIs:
+- Property schema: `Editor.getProperty / upsertProperty / removeProperty`
+- Values on blocks: `Editor.upsertBlockProperty / removeBlockProperty / getBlockProperties`
+
+---
+
+## 2) Important types (from `LSPlugin.ts`)
+
+### `PropertySchema`
+```ts
+export type PropertySchema = {
+  type: 'default' | 'number' | 'node' | 'date' | 'checkbox' | 'url' | string,
+  cardinality: 'many' | 'one',
+  hide: boolean
+  public: boolean
+}
+```
+
+Practical meaning:
+- `type`
+  - Controls editor/UI behavior and (in DB graphs) how values are interpreted.
+  - Common: `default`, `number`, `date`, `checkbox`, `url`, `node`.
+- `cardinality`
+  - `'one'`: a single value
+  - `'many'`: multiple values (typically passed as an array)
+- `hide`
+  - Hide in UI property panels.
+- `public`
+  - Expose property in UI (and typically configure discoverability).
+
+### Entities you’ll see
+- `BlockEntity`: blocks, but also used for some “special blocks” such as property entities.
+- `PageEntity`: pages, tags (classes), property pages (`type: 'property'`), etc.
+- `BlockIdentity`: a block uuid string OR `{ uuid }`.
+
+---
+
+## 3) DB-only vs graph-agnostic checks
+
+Before doing DB-only work, check graph type:
+
+```ts
+const isDbGraph = await logseq.App.checkCurrentIsDbGraph()
+if (!isDbGraph) {
+  await logseq.UI.showMsg('This feature requires a DB graph.', 'warning')
+  return
+}
+
+// Also check app-level capability:
+const { supportDb } = await logseq.App.getInfo()
+```
+
+---
+
+## 4) Property schema APIs (DB only)
+
+### Get a property definition
+```ts
+const propEntity = await logseq.Editor.getProperty('zotero_key')
+// -> BlockEntity | null
+```
+
+### Create or update a property definition (idempotent)
+Use `upsertProperty` to ensure the property exists and has the schema you expect.
+
+```ts
+await logseq.Editor.upsertProperty(
+  'zotero_key',
+  {
+    type: 'default',
+    cardinality: 'one',
+    hide: false,
+    public: true,
+  },
+  { name: 'Zotero Key' }
+)
+```
+
+Notes:
+- `key` is your stable identifier (recommend `snake_case`).
+- `opts.name` can be used as a **display name** for users.
+
+### Remove a property definition
+```ts
+await logseq.Editor.removeProperty('zotero_key')
+```
+
+---
+
+## 5) Block/page property value APIs
+
+### Set (upsert) a property value on a block
+```ts
+const block = await logseq.Editor.getCurrentBlock()
+if (!block) return
+
+await logseq.Editor.upsertBlockProperty(block.uuid, 'zotero_key', 'ABCD1234')
+```
+
+### Set multi-value properties (`cardinality: 'many'`)
+For `many`, pass an array. Use `{ reset: true }` when you want to overwrite vs merge.
+
+```ts
+await logseq.Editor.upsertBlockProperty(
+  block.uuid,
+  'authors',
+  ['Ada Lovelace', 'Alan Turing'],
+  { reset: true }
+)
+```
+
+### Remove a value
+```ts
+await logseq.Editor.removeBlockProperty(block.uuid, 'zotero_key')
+```
+
+### Read properties
+```ts
+const props = await logseq.Editor.getBlockProperties(block.uuid)
+// -> Record<string, any> | null
+
+const pageProps = await logseq.Editor.getPageProperties('My Page')
+```
+
+### Read a single property (returns a `BlockEntity | null`)
+`getBlockProperty` is useful when you want the DB entity wrapper for a key.
+
+```ts
+const v = await logseq.Editor.getBlockProperty(block.uuid, 'zotero_key')
+```
+
+---
+
+## 6) Tags as “classes” + attaching properties (DB modeling)
+
+In DB graphs, tags can behave like **classes**.
+
+Relevant APIs:
+- `Editor.createTag(tagName, opts)`
+- `Editor.addTagProperty(tagId, propertyIdOrName)` / `removeTagProperty`
+- `Editor.addTagExtends(tagId, parentTagIdOrName)` / `removeTagExtends`
+- `Editor.addBlockTag(blockId, tagId)` / `removeBlockTag`
+
+### Create a tag with tagProperties
+This is the most developer-friendly way to ship a “schema bundle”:
+
+```ts
+const tag = await logseq.Editor.createTag('ZoteroItem', {
+  tagProperties: [
+    {
+      name: 'zotero_key',
+      schema: { type: 'default', cardinality: 'one', public: true, hide: false },
+    },
+    {
+      name: 'authors',
+      schema: { type: 'node', cardinality: 'many', public: true, hide: false },
+    },
+    {
+      name: 'published_at',
+      schema: { type: 'date', cardinality: 'one', public: true, hide: false },
+    },
+  ],
+})
+
+if (tag) {
+  const block = await logseq.Editor.getCurrentBlock()
+  if (block) await logseq.Editor.addBlockTag(block.uuid, tag.uuid)
+}
+```
+
+Why this pattern is popular:
+- Users see a consistent “record type” (`ZoteroItem`) with fields.
+- Plugins can be idempotent: create if missing, otherwise just reuse.
+
+### Add properties to an existing tag later
+```ts
+const tag = await logseq.Editor.getTag('ZoteroItem')
+if (tag) {
+  await logseq.Editor.addTagProperty(tag.uuid, 'zotero_key')
+  await logseq.Editor.addTagProperty(tag.uuid, 'authors')
+}
+```
+
+### Tag inheritance
+```ts
+await logseq.Editor.addTagExtends('ZoteroItem', 'Reference')
+```
+
+---
+
+## 7) Recommended conventions for plugin authors
+
+### Prefer DB APIs over text editing in DB graphs
+- Don’t generate `key:: value` strings as your “source of truth” in DB mode.
+- Use `upsertProperty` + `upsertBlockProperty`.
+
+### Keep keys stable; use display names for UX
+- Use safe keys (`snake_case`, ASCII) for `key`.
+- Use `upsertProperty(key, schema, { name })` to show a nice label.
+
+### Be explicit about cardinality
+- If you intend a list, set `cardinality: 'many'` and always write arrays.
+- Use `{ reset: true }` when you want to overwrite.
+
+### Guard DB-only APIs
+- Always check `App.checkCurrentIsDbGraph()`.
+
+---
+
+## 8) A tiny helper wrapper (optional)
+
+If you’re building a small internal “DB properties SDK” for your plugin, a minimal shape looks like:
+
+```ts
+export async function ensureDbGraph() {
+  if (!(await logseq.App.checkCurrentIsDbGraph())) {
+    throw new Error('DB graph required')
+  }
+}
+
+export async function ensureProperty(
+  key: string,
+  schema: Partial<import('../src/LSPlugin').PropertySchema>,
+  name?: string
+) {
+  return logseq.Editor.upsertProperty(key, schema, name ? { name } : undefined)
+}
+
+export async function setProp(
+  block: string,
+  key: string,
+  value: any,
+  reset = false
+) {
+  return logseq.Editor.upsertBlockProperty(block, key, value, reset ? { reset: true } : undefined)
+}
+```
+
+---
+
+## Appendix: API quick reference (from `LSPlugin.ts`)
+
+**Property schema (DB only)**
+- `Editor.getProperty(key)`
+- `Editor.upsertProperty(key, schema?, opts?)`
+- `Editor.removeProperty(key)`
+
+**Block/page values**
+- `Editor.upsertBlockProperty(block, key, value, { reset? })`
+- `Editor.removeBlockProperty(block, key)`
+- `Editor.getBlockProperty(block, key)`
+- `Editor.getBlockProperties(block)`
+- `Editor.getPageProperties(page)`
+
+**Tags / Classes**
+- `Editor.createTag(tagName, { tagProperties? })`
+- `Editor.getTag(nameOrIdent)` / `Editor.getAllTags()`
+- `Editor.addTagProperty(tagId, propertyIdOrName)` / `removeTagProperty`
+- `Editor.addTagExtends(tagId, parentTagIdOrName)` / `removeTagExtends`
+- `Editor.addBlockTag(blockId, tagId)` / `removeBlockTag`
+

+ 406 - 0
libs/development-notes/db_properties_skill.md

@@ -0,0 +1,406 @@
+# Logseq DB Properties SDK Reference
+
+## Overview
+
+Logseq has two storage modes:
+- **File-based**: Properties stored as `key::  value` in markdown files
+- **DB-based**:  Properties stored in SQLite with structured schema, types, and relationships
+
+This guide covers **DB-based properties API** for plugin development.
+
+---
+
+## Core Concepts
+
+### 1. Property Schema (Global Definition)
+Properties must be defined globally before use.  Think of it as creating columns in a database.
+
+### 2. Tags as Classes
+Tags act as classes/types.  Pages/blocks can be tagged to inherit property schemas.
+
+### 3. Property Values
+After defining properties, assign values to specific blocks/pages.
+
+---
+
+## API Reference
+
+### Property Schema APIs
+
+#### `upsertProperty(name, options)`
+Define or update a property schema globally.
+
+**Parameters:**
+- `name` (string): Property name
+- `options` (object):
+    - `type`: `'default'` | `'node'` | `'date'` | `'number'`
+    - `cardinality`: `'one'` | `'many'` (for `node` type)
+
+**Returns:** Property object with `uuid`
+
+**Example:**
+```typescript
+// Simple text property
+await logseq.Editor.upsertProperty('author', { type: 'default' })
+
+// Reference property (links to other pages)
+await logseq.Editor.upsertProperty('tags', { 
+  type: 'node', 
+  cardinality: 'many' 
+})
+
+// Date property
+await logseq.Editor.upsertProperty('publishedDate', { type: 'date' })
+```
+
+---
+
+### Tag (Class) Management APIs
+
+#### `createTag(name, options? )`
+Create a tag as a class/type definition.
+
+**Parameters:**
+- `name` (string): Tag name
+- `options` (optional object): `{ uuid: string }`
+
+**Returns:** Tag object with `uuid` and `id`
+
+**Example:**
+```typescript
+const bookTag = await logseq.Editor.createTag('book')
+const articleTag = await logseq.Editor.createTag('article', { 
+  uuid: 'custom-uuid-123' 
+})
+```
+
+#### `getTag(name)`
+Retrieve an existing tag by name.
+
+**Returns:** Tag object or `null`
+
+**Example:**
+```typescript
+const tag = await logseq.Editor. getTag('book')
+if (! tag) {
+  // Tag doesn't exist, create it
+}
+```
+
+#### `addTagProperty(tagUUID, propertyName)`
+Add a property to a tag's schema.  Pages tagged with this tag will have this property available.
+
+**Parameters:**
+- `tagUUID` (string): Tag UUID
+- `propertyName` (string): Property name (must be defined via `upsertProperty` first)
+
+**Example:**
+```typescript
+// Define properties globally
+await logseq.Editor. upsertProperty('author', { type: 'default' })
+await logseq.Editor.upsertProperty('isbn', { type: 'default' })
+
+// Add properties to 'book' tag
+const bookTag = await logseq. Editor.createTag('book')
+await logseq.Editor.addTagProperty(bookTag.uuid, 'author')
+await logseq.Editor.addTagProperty(bookTag.uuid, 'isbn')
+```
+
+---
+
+### Block/Page Property Value APIs
+
+#### `upsertBlockProperty(blockUUID, propertyName, value)`
+Set property value for a specific block or page.
+
+**Parameters:**
+- `blockUUID` (string): Block or page UUID
+- `propertyName` (string): Property name
+- `value` (any): Property value
+    - For `node` type with `cardinality: 'many'`: Use array of page IDs
+    - For `node` type with `cardinality: 'one'`: Use single page ID
+    - For other types: Use primitive values
+
+**Example:**
+```typescript
+const page = await logseq.Editor.getPage('my-page-uuid')
+
+// Set simple value
+await logseq.Editor.upsertBlockProperty(page.uuid, 'author', 'John Doe')
+
+// Set number
+await logseq.Editor.upsertBlockProperty(page.uuid, 'year', 2024)
+
+// Set single reference
+await logseq.Editor. upsertBlockProperty(page.uuid, 'category', categoryPageID)
+
+// Set multiple references
+await logseq.Editor.upsertBlockProperty(
+  page.uuid, 
+  'tags', 
+  [tagPageID1, tagPageID2, tagPageID3]
+)
+```
+
+#### `addBlockTag(blockUUID, tagUUID)`
+Tag a block or page, making it an instance of that tag's class.
+
+**Parameters:**
+- `blockUUID` (string): Block or page UUID
+- `tagUUID` (string): Tag UUID
+
+**Example:**
+```typescript
+const page = await logseq. Editor.createPage('Moby Dick')
+const bookTag = await logseq.Editor.getTag('book')
+await logseq.Editor.addBlockTag(page.uuid, bookTag.uuid)
+```
+
+---
+
+### Tag Inheritance
+
+#### Set Tag Parent (Class Inheritance)
+Invoke API of `logseq.Editor.setTagExtends`
+
+**Example:**
+```typescript
+// Create parent tag
+const rootTag = await logseq.Editor.createTag('Media')
+
+// Create child tag
+const bookTag = await logseq.Editor.createTag('book')
+
+// Set inheritance
+await logseq.Editor.addTagExtends(bookTag.uuid, rootTag.uuid)
+
+// Use special property `:logseq.property.class/extends` to create tag hierarchy.
+// await logseq.Editor. upsertBlockProperty(
+//   bookTag.uuid, 
+//   ':logseq.property.class/extends', 
+//   [rootTag.id]  // Use . id, not .uuid
+// )
+```
+
+---
+
+### Special Properties
+
+#### Hide Empty Values
+```typescript
+const prop = await logseq.Editor. upsertProperty('optionalField', { type: 'default' })
+await logseq.Editor.upsertBlockProperty(
+  prop.uuid, 
+  ': logseq.property/hide-empty-value', 
+  true
+)
+```
+
+---
+
+## Common Patterns
+
+### Pattern 1: Define a Schema with Class Hierarchy
+
+```typescript
+async function setupBookSchema() {
+  // 1. Create root class
+  const mediaTag = await logseq.Editor.createTag('Media')
+  
+  // 2. Define common properties
+  await logseq. Editor.upsertProperty('title', { type: 'default' })
+  await logseq.Editor.upsertProperty('year', { type: 'number' })
+  await logseq.Editor.upsertProperty('tags', { type: 'node', cardinality: 'many' })
+  
+  // 3. Add properties to root class
+  await logseq. Editor.addTagProperty(mediaTag.uuid, 'title')
+  await logseq.Editor.addTagProperty(mediaTag.uuid, 'year')
+  await logseq.Editor. addTagProperty(mediaTag.uuid, 'tags')
+  
+  // 4. Create subclass
+  const bookTag = await logseq.Editor.createTag('book')
+  await logseq.Editor.upsertBlockProperty(
+    bookTag.uuid, 
+    ':logseq.property.class/extends', 
+    [mediaTag.id]
+  )
+  
+  // 5. Add book-specific properties
+  await logseq.Editor.upsertProperty('author', { type: 'default' })
+  await logseq.Editor.upsertProperty('isbn', { type: 'default' })
+  await logseq.Editor.addTagProperty(bookTag.uuid, 'author')
+  await logseq.Editor.addTagProperty(bookTag.uuid, 'isbn')
+}
+```
+
+### Pattern 2: Create an Instance
+
+```typescript
+async function createBookInstance(bookData) {
+  // 1. Create page with stable UUID
+  const pageUUID = generateStableUUID(bookData.id)
+  let page = await logseq.Editor.getPage(pageUUID)
+  
+  if (!page) {
+    page = await logseq.Editor.createPage(
+      bookData.title, 
+      {}, 
+      { customUUID: pageUUID, redirect: false }
+    )
+  }
+  
+  // 2. Tag the page
+  const bookTag = await logseq.Editor.getTag('book')
+  await logseq.Editor.addBlockTag(page.uuid, bookTag. uuid)
+  
+  // 3. Set property values
+  await logseq. Editor.upsertBlockProperty(page.uuid, 'title', bookData.title)
+  await logseq.Editor.upsertBlockProperty(page.uuid, 'author', bookData.author)
+  await logseq.Editor.upsertBlockProperty(page.uuid, 'isbn', bookData.isbn)
+  await logseq.Editor.upsertBlockProperty(page.uuid, 'year', bookData.year)
+  
+  return page. uuid
+}
+```
+
+### Pattern 3: Link Related Entities
+
+```typescript
+async function linkBookToCategories(bookPageUUID, categoryKeys) {
+  // Get or create category pages
+  const categoryIDs = await Promise.all(
+    categoryKeys.map(async (key) => {
+      const catUUID = generateStableUUID(key)
+      let catPage = await logseq.Editor.getPage(catUUID)
+      
+      if (!catPage) {
+        catPage = await logseq.Editor.createPage(key, {}, { 
+          customUUID: catUUID 
+        })
+      }
+      
+      return catPage.id  // Use . id for references
+    })
+  )
+  
+  // Link book to categories
+  await logseq.Editor.upsertBlockProperty(
+    bookPageUUID, 
+    'tags',  // Must be 'node' type with 'many' cardinality
+    categoryIDs
+  )
+}
+```
+
+### Pattern 4: Batch Schema Setup from JSON
+
+```typescript
+async function setupSchemaFromConfig(config) {
+  // config = { "book": { "fields": ["author", "isbn"], "parent": "Media" } }
+  
+  for (const [tagName, tagConfig] of Object.entries(config)) {
+    let tag = await logseq.Editor.getTag(tagName)
+    
+    if (!tag) {
+      tag = await logseq.Editor.createTag(tagName)
+      
+      // Set parent if specified
+      if (tagConfig. parent) {
+        const parentTag = await logseq. Editor.getTag(tagConfig. parent)
+        if (parentTag) {
+          await logseq.Editor.upsertBlockProperty(
+            tag. uuid, 
+            ':logseq.property.class/extends', 
+            [parentTag.id]
+          )
+        }
+      }
+    }
+    
+    // Add properties
+    for (const fieldName of tagConfig.fields) {
+      await logseq.Editor.upsertProperty(fieldName, { type: 'default' })
+      await logseq.Editor.addTagProperty(tag.uuid, fieldName)
+    }
+  }
+}
+```
+
+---
+
+## Best Practices
+
+### 1. Use Stable UUIDs
+Generate deterministic UUIDs from stable identifiers:
+
+```typescript
+import { v5 as uuidv5 } from 'uuid'
+
+const NAMESPACE = 'your-plugin-namespace-uuid'
+
+function generateStableUUID(id:  string): string {
+  return uuidv5(id, NAMESPACE)
+}
+```
+
+### 2. Check Before Creating
+Always check if tags/pages exist before creating:
+
+```typescript
+let tag = await logseq.Editor.getTag('book')
+if (!tag) {
+  tag = await logseq.Editor.createTag('book')
+}
+```
+
+### 3. Property Definition Order
+1. Define property globally with `upsertProperty`
+2. Create tag with `createTag`
+3. Add property to tag with `addTagProperty`
+4. Create page and tag it with `addBlockTag`
+5. Set values with `upsertBlockProperty`
+
+### 4. Use . id for References
+When setting `node` type properties, use `.id` not `.uuid`:
+
+```typescript
+await logseq.Editor.upsertBlockProperty(
+  page. uuid, 
+  'category', 
+  categoryPage.id  // ✅ Correct
+  // categoryPage.uuid  // ❌ Wrong
+)
+```
+
+### 5. Error Handling
+Wrap API calls in try-catch for robustness:
+
+```typescript
+try {
+  await logseq.Editor.upsertBlockProperty(uuid, 'field', value)
+} catch (error) {
+  console.error(`Failed to set property: ${error}`)
+  // Handle gracefully
+}
+```
+
+---
+
+## Real-World Example:  Zotero Plugin
+
+See complete implementation:  [xyhp915/logseq-zotero-plugin](https://github.com/xyhp915/logseq-zotero-plugin/blob/main/src/handlers.ts#L19-L207)
+
+Key techniques:
+- Schema defined from `z_item_types. json` metadata
+- Tag hierarchy:  `Zotero` → `book`, `article`, etc.
+- Stable UUIDs via `v5(itemKey, namespace)`
+- Relationship properties for collections
+- Nested blocks for attachments
+
+---
+
+## Reference
+
+- **Type Definitions**: See `@logseq/libs` package
+- **Examples**: [logseq-zotero-plugin/handlers.ts](https://github.com/xyhp915/logseq-zotero-plugin/blob/main/src/handlers. ts)
+- **Search More**: [GitHub Code Search](https://github.com/search?type=code&q=repo:xyhp915/logseq-zotero-plugin+upsertBlockProperty)

+ 1 - 1
libs/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@logseq/libs",
-  "version": "0.2.10",
+  "version": "0.2.11",
   "description": "Logseq SDK libraries",
   "main": "dist/lsplugin.user.js",
   "typings": "index.d.ts",